F# 5의 새로운 기능

F# 5는 F# 언어 및 F# 대화형에 몇 가지 개선 사항을 추가합니다. .NET 5와 함께 릴리스됩니다.

.NET 다운로드 페이지에서 최신 .NET SDK를 다운로드할 수 있습니다.

시작하기

F# 5는 모든 .NET Core 배포 및 Visual Studio 도구에서 사용할 수 있습니다. 자세한 내용은 F# 으로 시작하기를 참조하세요.

F# 스크립트의 패키지 참조

F# 5는 구문을 사용하여 F# 스크립트에서 패키지 참조를 지원합니다 #r "nuget:..." . 예를 들어 다음 패키지 참조를 고려합니다.

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let o = {| X = 2; Y = "Hello" |}

printfn $"{JsonConvert.SerializeObject o}"

다음과 같이 패키지 이름 뒤의 명시적 버전을 제공할 수도 있습니다.

#r "nuget: Newtonsoft.Json,11.0.1"

패키지 참조는 ML.NET 같은 네이티브 종속성이 있는 패키지를 지원합니다.

또한 패키지 참조는 종속 .dlls 참조에 대한 특별한 요구 사항이 있는 패키지를 지원합니다. 예를 들어 FParsec 패키지는 사용자가 F# Interactive에서 참조되기 전에 FParsec.dll 해당 종속 FParsecCS.dll 성이 먼저 참조되었는지 수동으로 확인하도록 요구하는 데 사용됩니다. 더 이상 필요하지 않으며 다음과 같이 패키지를 참조할 수 있습니다.

#r "nuget: FParsec"

open FParsec

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn $"Success: {result}"
    | Failure(errorMsg, _, _) -> printfn $"Failure: {errorMsg}"

test pfloat "1.234"

이 기능은 F# 도구 RFC FST-1027을 구현합니다. 패키지 참조에 대한 자세한 내용은 F# 대화형 자습서를 참조하세요.

문자열 보간

F# 보간된 문자열은 C# 또는 JavaScript 보간된 문자열과 매우 유사합니다. 즉, 문자열 리터럴 내의 "구멍"에 코드를 작성할 수 있습니다. 기본 예제는 다음과 같습니다.

let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"

printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"

그러나 F# 보간된 문자열은 함수와 마찬가지로 sprintf 형식화된 보간을 허용하여 보간된 컨텍스트 내의 식이 특정 형식을 준수하도록 합니다. 동일한 형식 지정자를 사용합니다.

let name = "Phillip"
let age = 29

printfn $"Name: %s{name}, Age: %d{age}"

// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"

위의 형식화된 보간 예제 %s 에서는 보간이 형식 string이어야 하는 반면 %d 보간은 형식이어야 integer합니다.

또한 임의의 F# 식(또는 식)을 보간 컨텍스트의 측면에 배치할 수 있습니다. 다음과 같이 더 복잡한 식을 작성할 수도 있습니다.

let str =
    $"""The result of squaring each odd item in {[1..10]} is:
{
    let square x = x * x
    let isOdd x = x % 2 <> 0
    let oddSquares xs =
        xs
        |> List.filter isOdd
        |> List.map square
    oddSquares [1..10]
}
"""

실제로는 이 작업을 너무 많이 수행하지 않는 것이 좋습니다.

이 기능은 F# RFC FS-1001을 구현합니다.

nameof에 대한 지원

F# 5는 nameof 사용 중인 기호를 확인하고 F# 원본에서 해당 이름을 생성하는 연산자를 지원합니다. 이는 로깅과 같은 다양한 시나리오에서 유용하며 소스 코드의 변경으로부터 로깅을 보호합니다.

let months =
    [
        "January"; "February"; "March"; "April";
        "May"; "June"; "July"; "August"; "September";
        "October"; "November"; "December"
    ]

let lookupMonth month =
    if (month > 12 || month < 1) then
        invalidArg (nameof month) (sprintf "Value passed in was %d." month)

    months[month-1]

printfn $"{lookupMonth 12}"
printfn $"{lookupMonth 1}"
printfn $"{lookupMonth 13}"

마지막 줄에 예외가 발생하며 오류 메시지에 "month"가 표시됩니다.

거의 모든 F# 구문의 이름을 사용할 수 있습니다.

module M =
    let f x = nameof x

printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"

세 가지 최종 추가 사항은 연산자의 작동 방식에 대한 변경 사항입니다. 제네릭 형식 매개 변수에 대한 폼 추가 nameof<'type-parameter> 및 패턴 일치 식에서 패턴으로 사용할 nameof 수 있는 기능입니다.

연산자의 이름을 지정하면 원본 문자열이 지정됩니다. 컴파일된 양식이 필요한 경우 연산자의 컴파일된 이름을 사용합니다.

nameof(+) // "+"
nameof op_Addition // "op_Addition"

형식 매개 변수의 이름을 사용하려면 약간 다른 구문이 필요합니다.

type C<'TType> =
    member _.TypeName = nameof<'TType>

이는 연산자 및 연산자와 typeof<'T>typedefof<'T> 비슷합니다.

F# 5에서는 식에 사용할 수 있는 패턴에 match 대한 nameof 지원도 추가합니다.

[<Struct; IsByRefLike>]
type RecordedEvent = { EventType: string; Data: ReadOnlySpan<byte> }

type MyEvent =
    | AData of int
    | BData of string

let deserialize (e: RecordedEvent) : MyEvent =
    match e.EventType with
    | nameof AData -> AData (JsonSerializer.Deserialize<int> e.Data)
    | nameof BData -> BData (JsonSerializer.Deserialize<string> e.Data)
    | t -> failwithf "Invalid EventType: %s" t

앞의 코드는 일치 식에서 문자열 리터럴 대신 'nameof'를 사용합니다.

이 기능은 F# RFC FS-1003을 구현합니다.

형식 선언 열기

또한 F# 5는 개방형 형식 선언에 대한 지원을 추가합니다. 개방형 형식 선언은 C#에서 정적 클래스를 여는 것과 같습니다. 단, F# 의미 체계에 맞게 약간 다른 구문과 약간 다른 동작이 있습니다.

열려 있는 형식 선언을 사용하면 모든 형식을 사용하여 내부 정적 콘텐츠를 노출할 수 있습니다 open . 또한 F#정의 공용 구조체 및 레코드를 사용하여 콘텐츠를 노출할 수 open 있습니다. 예를 들어 모듈에 공용 구조체가 정의되어 있고 해당 사례에 액세스하려고 하지만 전체 모듈을 열지 않으려는 경우에 유용할 수 있습니다.

open type System.Math

let x = Min(1.0, 2.0)

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn $"{A}"

C#과 달리 이름이 같은 멤버를 노출하는 두 형식의 경우 open type ed인 open마지막 형식의 멤버가 다른 이름을 숨깁니다. 이는 이미 존재하는 그림자에 대한 F# 의미 체계와 일치합니다.

이 기능은 F# RFC FS-1068을 구현합니다.

기본 제공 데이터 형식에 대한 일관된 조각화 동작

F# 5 이전의 일관성이 없는 기본 제공 FSharp.Core 데이터 형식(배열, 목록, 문자열, 2D 배열, 3D 배열, 4D 배열)을 조각화하기 위한 동작입니다. 일부 에지 대/소문자 동작은 예외를 throw했고 일부는 예외를 throw하지 않았습니다. 이제 F# 5에서 모든 기본 제공 형식은 생성할 수 없는 조각에 대해 빈 조각을 반환합니다.

let l = [ 1..10 ]
let a = [| 1..10 |]
let s = "hello!"

// Before: would return empty list
// F# 5: same
let emptyList = l[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty array
let emptyArray = a[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty string
let emptyString = s[-2..(-1)]

이 기능은 F# RFC FS-1077을 구현합니다.

FSharp.Core의 3D 및 4D 배열에 대한 고정 인덱스 조각

F# 5는 기본 제공 3D 및 4D 배열 형식에서 고정 인덱스로 조각화할 수 있습니다.

이를 설명하기 위해 다음 3D 배열을 고려합니다.

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

배열에서 조각을 [| 4; 5 |] 추출하려면 어떻게 해야 할까요? 이것은 이제 매우 간단합니다!

// First, create a 3D array to slice

let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable count = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m[x,y,z] <- count
            count <- count + 1

// Now let's get the [4;5] slice!
m[*, 0, 1]

이 기능은 F# RFC FS-1077b를 구현 합니다.

F# 따옴표 개선 사항

이제 F# 코드 따옴표에 형식 제약 조건 정보를 유지할 수 있습니다. 다음 예제를 참조하세요.

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

함수에서 inline 생성된 제약 조건은 코드 따옴표로 유지됩니다. negate 이제 함수의 따옴표 붙은 폼을 평가할 수 있습니다.

이 기능은 F# RFC FS-1071을 구현합니다.

적용 계산 식

CES(계산 식) 는 오늘날 "상황별 계산"을 모델링하거나 더 기능적인 프로그래밍 친화적인 용어인 모나딕 계산에서 사용됩니다.

F# 5에는 다른 계산 모델을 제공하는 적용 CE가 도입되었습니다. 적용 CES를 사용하면 모든 계산이 독립적이며 결과가 마지막에 누적되는 경우 보다 효율적인 계산을 할 수 있습니다. 계산이 서로 독립적이면 쉽게 병렬 처리할 수 있으므로 CE 작성자가 보다 효율적인 라이브러리를 작성할 수 있습니다. 그러나 이 이점은 이전에 계산된 값에 의존하는 계산은 허용되지 않는다는 제한 사항이 있습니다.

다음 예제에서는 형식에 대한 기본 적용 CE를 Result 보여줍니다.

// First, define a 'zip' function
module Result =
    let zip x1 x2 =
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }

    match res1 with
    | Ok x -> printfn $"{nameof res1} is: %d{x}"
    | Error e -> printfn $"{nameof res1} is: {e}"

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

현재 라이브러리에서 CE를 노출하는 라이브러리 작성자인 경우 몇 가지 추가 고려 사항을 알아야 합니다.

이 기능은 F# RFC FS-1063을 구현합니다.

인터페이스는 다른 제네릭 인스턴스화에서 구현할 수 있습니다.

이제 다른 제네릭 인스턴스화에서 동일한 인터페이스를 구현할 수 있습니다.

type IA<'T> =
    abstract member Get : unit -> 'T

type MyClass() =
    interface IA<int> with
        member x.Get() = 1
    interface IA<string> with
        member x.Get() = "hello"

let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>

iaInt.Get() // 1
iaString.Get() // "hello"

이 기능은 F# RFC FS-1031을 구현합니다.

기본 인터페이스 멤버 사용

F# 5를 사용하면 기본 구현을 사용하는 인터페이스를 사용할 수 있습니다.

다음과 같이 C#에 정의된 인터페이스를 고려합니다.

using System;

namespace CSharp
{
    public interface MyDim
    {
        public int Z => 0;
    }
}

인터페이스를 구현하는 표준 방법을 통해 F#에서 사용할 수 있습니다.

open CSharp

// You can implement the interface via a class
type MyType() =
    member _.M() = ()

    interface MyDim

let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"

// You can also implement it via an object expression
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"

이렇게 하면 사용자가 기본 구현을 사용할 수 있을 것으로 예상할 때 최신 C#으로 작성된 C# 코드 및 .NET 구성 요소를 안전하게 활용할 수 있습니다.

이 기능은 F# RFC FS-1074를 구현합니다.

nullable 값 형식을 사용하여 간소화된 interop

Nullable(값) 형식(지금까지 Nullable 형식이라고 함)은 F#에서 오랫동안 지원해 왔지만 값을 전달할 때마다 래퍼를 Nullable<SomeType> 생성 Nullable 해야 하므로 일반적으로 이러한 형식과 상호 작용하는 것은 다소 고통스러웠습니다. 이제 컴파일러는 대상 형식이 일치하는 경우 값 형식을 Nullable<ThatValueType> 암시적으로 변환합니다. 이제 다음 코드가 가능합니다.

#r "nuget: Microsoft.Data.Analysis"

open Microsoft.Data.Analysis

let dateTimes = PrimitiveDataFrameColumn<DateTime>("DateTimes")

// The following line used to fail to compile
dateTimes.Append(DateTime.Parse("2019/01/01"))

// The previous line is now equivalent to this line
dateTimes.Append(Nullable<DateTime>(DateTime.Parse("2019/01/01")))

이 기능은 F# RFC FS-1075를 구현합니다.

미리 보기: 역방향 인덱스

또한 F# 5에서는 역방향 인덱스를 허용하기 위한 미리 보기가 도입되었습니다. 구문은 ^idx입니다. 목록의 끝에서 요소 1 값을 만드는 방법은 다음과 같습니다.

let xs = [1..10]

// Get element 1 from the end:
xs[^1]

// From the end slices

let lastTwoOldStyle = xs[(xs.Length-2)..]

let lastTwoNewStyle = xs[^1..]

lastTwoOldStyle = lastTwoNewStyle // true

고유한 형식에 대한 역방향 인덱스를 정의할 수도 있습니다. 이렇게 하려면 다음 방법을 구현해야 합니다.

GetReverseIndex: dimension: int -> offset: int

다음은 형식의 예입니다.Span<'T>

open System

type Span<'T> with
    member sp.GetSlice(startIdx, endIdx) =
        let s = defaultArg startIdx 0
        let e = defaultArg endIdx sp.Length
        sp.Slice(s, e - s)

    member sp.GetReverseIndex(_, offset: int) =
        sp.Length - offset

let printSpan (sp: Span<int>) =
    let arr = sp.ToArray()
    printfn $"{arr}"

let run () =
    let sp = [| 1; 2; 3; 4; 5 |].AsSpan()

    // Pre-# 5.0 slicing on a Span<'T>
    printSpan sp[0..] // [|1; 2; 3; 4; 5|]
    printSpan sp[..3] // [|1; 2; 3|]
    printSpan sp[1..3] // |2; 3|]

    // Same slices, but only using from-the-end index
    printSpan sp[..^0] // [|1; 2; 3; 4; 5|]
    printSpan sp[..^2] // [|1; 2; 3|]
    printSpan sp[^4..^2] // [|2; 3|]

run() // Prints the same thing twice

이 기능은 F# RFC FS-1076을 구현합니다.

미리 보기: 계산 식에서 사용자 지정 키워드(keyword) 오버로드

계산 식은 라이브러리 및 프레임워크 작성자에게 강력한 기능입니다. 이를 통해 잘 알려진 멤버를 정의하고 작업 중인 기본 DSL을 형성하여 구성 요소의 표현력을 크게 향상시킬 수 있습니다.

F# 5에서는 계산 식에서 사용자 지정 작업을 오버로드하기 위한 미리 보기 지원을 추가합니다. 다음 코드를 작성하고 사용할 수 있습니다.

open System

type InputKind =
    | Text of placeholder:string option
    | Password of placeholder: string option

type InputOptions =
  { Label: string option
    Kind : InputKind
    Validators : (string -> bool) array }

type InputBuilder() =
    member t.Yield(_) =
      { Label = None
        Kind = Text None
        Validators = [||] }

    [<CustomOperation("text")>]
    member this.Text(io, ?placeholder) =
        { io with Kind = Text placeholder }

    [<CustomOperation("password")>]
    member this.Password(io, ?placeholder) =
        { io with Kind = Password placeholder }

    [<CustomOperation("label")>]
    member this.Label(io, label) =
        { io with Label = Some label }

    [<CustomOperation("with_validators")>]
    member this.Validators(io, [<ParamArray>] validators) =
        { io with Validators = validators }

let input = InputBuilder()

let name =
    input {
    label "Name"
    text
    with_validators
        (String.IsNullOrWhiteSpace >> not)
    }

let email =
    input {
    label "Email"
    text "Your email"
    with_validators
        (String.IsNullOrWhiteSpace >> not)
        (fun s -> s.Contains "@")
    }

let password =
    input {
    label "Password"
    password "Must contains at least 6 characters, one number and one uppercase"
    with_validators
        (String.exists Char.IsUpper)
        (String.exists Char.IsDigit)
        (fun s -> s.Length >= 6)
    }

이 변경 전에 형식을 InputBuilder 있는 그대로 작성할 수 있지만 예제에서 사용되는 방식으로는 사용할 수 없습니다. 오버로드, 선택적 매개 변수 및 이제 System.ParamArray 형식이 허용되므로 모든 것이 예상대로 작동합니다.

이 기능은 F# RFC FS-1056을 구현합니다.