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 套件用來要求使用者手動確保先參考其相依性 FParsecCS.dll,再於 F# 互動中參考 FParsec.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}"

最後一行會擲回例外狀況,錯誤訊息中會顯示「月份」。

您可以取得幾乎每個 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 任何類型以公開其中的靜態內容。 此外,您可以 open F# 定義的等位和記錄來公開其內容。 例如,如果您在模組中定義了等位,且想要存取其案例,但不想開啟整個模組,這非常有用。

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 時,來自最後一個 open 類型的成員會遮蔽另一個名稱。 這與已存在陰影周圍的 F# 語意一致。

這項功能會實作 F# RFC FS-1068

內建資料類型的一致切割行為

分割內建 FSharp.Core 資料類型 (陣列、清單、字串、2D 陣列、3D 陣列、4D 陣列) 而在 F# 5 之前不一致的行為。 某些極端案例行為擲回例外狀況,有些則不會。 在 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。 適用的 CE 允許更有效率的計算,前提是每個計算都是獨立的,且其結果會在結尾累積。 當計算彼此獨立時,計算也會呈現簡單式平行,可讓 CE 作者撰寫更有效率的程式庫。 不過,這項優點有所限制:不允許相依於先前計算值的計算。

下列範例顯示 Result 類型的基本適用 CE。

// 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

具有可為 Null 實值型別的簡化 Interop

可為 Null 的 (值) 類型 (在過去稱為可為 Null 的型別) 長期受到 F# 支援,但與其互動在傳統上有點麻煩,因為每次想要傳遞值時都必須建構 NullableNullable<SomeType> 包裝函式。 現在,如果目標型別相符,編譯器就會隱含地將實值型別轉換為 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

預覽:計算運算式中自訂關鍵字的多載

計算運算式是程式庫和架構作者的強大功能。 其可讓您藉由定義已知的成員,並為您正在使用的網域形成 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