F# 6 的最新功能

F# 6 針對 F# 語言和 F# Interactive新增數項改善。 它與 .NET 6 一起發行。

您可以從 .NET 下載頁面下載最新 .NET SDK。

開始使用

F# 6 適用於所有 .NET Core 散發套件和 Visual Studio 工具。 如需詳細資訊,請參閱<開始使用 F#>。

task {…}

F# 6 包含以 F# 程式碼撰寫 .NET 工作的原生支援。 例如,使用下列 F# 程式碼來建立與 .NET 相容的工作:

let readFilesTask (path1, path2) =
   async {
        let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
        let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
        return Array.append bytes1 bytes2
   } |> Async.StartAsTask

您可以使用 F# 6 重寫此程式碼,如下所示。

let readFilesTask (path1, path2) =
   task {
        let! bytes1 = File.ReadAllBytesAsync(path1)
        let! bytes2 = File.ReadAllBytesAsync(path2)
        return Array.append bytes1 bytes2
   }

F# 5 的工作支援是透過絕佳的 TaskBuilder.fs 和 Ply 程式庫來取得。 將程式碼遷移至內建支援應該很容易。 不過,有一些差異:內建支援和這些程式庫之間的命名空間和型別推斷稍有不同,而且可能需要一些額外的型別註釋。 如有必要,您仍然可以搭配 F# 6 使用這些社群程式庫,前提是您要明確參考這些社群程式庫,並開啟每個檔案中的正確命名空間。

使用 task {…} 與使用 async {…} 非常類似。 使用 task {…} 具有數個優於 async {…} 的優點:

  • task {...} 的額外負荷較低,可能會提高非同步工作快速執行的經常性程式碼路徑中的效能。
  • task {…} 的偵測步驟和堆疊追蹤較佳。
  • 與預期或產生工作的 .NET 套件交互操作會比較容易。

如果您熟悉 async {…},請注意一些差異:

  • task {…} 會立即將工作執行到第一個等候點。
  • task {…} 不會隱含傳播取消權杖。
  • task {…} 不會執行隱含取消檢查。
  • task {…} 不支援非同步 tailcalls。 這表示如果沒有交錯的非同步等候,遞迴使用 return! .. 可能會導致堆疊溢位。

一般而言,如果您交互操作使用工作的 .NET 程式庫,而且您不依賴非同步程式碼 tailcalls 或隱含解除標記傳播,則應該考慮在新的程式碼中使用 task {…},而不是使用 async {…}。 在現有的程式碼中,您應在檢閱程式碼後才切換為 task {…},以確保您不依賴先前提及的 async {…} 特性。

此功能會實作 F# RFC FS-1097

搭配 expr[idx] 使用更簡單的索引語法

F# 6 允許使用 expr[idx] 語法來編製索引和切割集合。

從 F# 5 開始,F# 已使用 expr.[idx] 作為索引編製語法。 允許使用 expr[idx] 建基於許多人初次學習 F# 或知道 F# 的反覆意見反應,他們認為使用點標記法似乎與標準產業實務之間有不必要的差異。

這不是中斷性變更,因為根據預設,使用 expr.[idx] 時不會發出任何警告。 不過會發出一些建議釐清程式碼的告知性訊息。 您也可以選擇性地啟動進一步的告知性訊息。 例如,您可以啟動選擇性的告知性警告 (/warnon:3566),開始報告 expr.[idx] 標記法的使用情況。 如需詳細資訊,請參閱索引子標記法

在新的程式碼中,我們建議以系統化方式使用 expr[idx] 作為索引語法。

此功能會實作 F# RFC FS-1110

部分現用模式的結構標記法

F# 6 使用部分現用模式的選擇性結構標記法來增強「現用模式」功能。 這可讓您使用屬性來限制部分現用模式,以傳回值選項:

[<return: Struct>]
let (|Int|_|) str =
   match System.Int32.TryParse(str) with
   | true, int -> ValueSome(int)
   | _ -> ValueNone

必須使用屬性。 在使用情況站台中,程式碼不會變更。 最終結果是配置減少。

此功能會實作 F# RFC FS-1039

計算運算式中的多載自訂作業

F# 6 可讓您在多載方法上使用 CustomOperationAttribute

請試想 content 計算運算式建立器的下列用法:

let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
    content {
        body "Name"
        body (ArraySegment<_>("Email"B, 0, 5))
        body "Password"B 2 4
        body "BYTES"B
        body mem
        body "Description" "of" "content"
    }

在這裡,body 自訂作業會採用各種數量且型別不同的引數。 實作下列使用多載的建立器可支援此做法:

type Content = ArraySegment<byte> list

type ContentBuilder() =
    member _.Run(c: Content) =
        let crlf = "\r\n"B
        [|for part in List.rev c do
            yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
            yield! crlf |]

    member _.Yield(_) = []

    [<CustomOperation("body")>]
    member _.Body(c: Content, segment: ArraySegment<byte>) =
        segment::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[]) =
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[], offset, count) =
        ArraySegment<byte>(bytes, offset, count)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, content: System.IO.Stream) =
        let mem = new System.IO.MemoryStream()
        content.CopyTo(mem)
        let bytes = mem.ToArray()
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, [<ParamArray>] contents: string[]) =
        List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c

此功能會實作 F# RFC FS-1056

“as” 模式

在 F# 6 中,as 模式的右側現在本身可以是模式。 當型別測試對輸入提供更強的型別時,此作法很重要。 例如,請考慮下列程式碼:

type Pair = Pair of int * int

let analyzeObject (input: obj) =
    match input with
    | :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
    | :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
    | _ -> printfn "Nope"

let input = box (1, 2)

在每個模式案例中,輸入物件都會經過型別測試。 as 模式的右側現在可成為更進階的模式,其本身可以符合型別更強的物件。

此功能會實作 F# RFC FS-1105

縮排語法修訂

F# 6 會在使用縮排感知語法時,將其中一些不一致和限制情況排除。 請參閱 RFC FS-1108。 這可解決 F# 4.0 之後,F# 使用者指出的 10 個重要問題。

例如,在 F# 5 中,可允許下列程式碼:

let c = (
    printfn "aaaa"
    printfn "bbbb"
)

不過,不允許下列程式碼 (會產生警告):

let c = [
    1
    2
]

F# 6 則可允許這兩種程式碼。 這可讓 F# 更簡單且更容易學習。 F# 社群參與者 Hadrian Tang 已在此之中領先一步,包括對功能進行絕佳且高度有價值的系統化測試。

此功能會實作 F# RFC FS-1108

其他隱含轉換

在 F# 6 中,我們已啟用其他「隱含」和「型別導向」轉換的支援,如 RFC FS-1093 中所述。

這項變更帶來三個優點:

  1. 需要較少的明確向上轉換
  2. 需要較少的明確整數轉換
  3. 新增 .NET 樣式隱含轉換的頂級支援

此功能會實作 F# RFC FS-1093

其他隱含向上轉換

F# 6 會實作額外的隱含向上轉換。 例如,在 F# 5 和舊版中,實作函式時,傳回運算式需要向上轉換,其中運算式的不同分支上有不同的子型別 (即使存在型別註釋也一樣)。 請試想下列 F# 5 程式碼:

open System
open System.IO

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt") :> TextReader

在這裡,條件式分支會分別計算 TextReaderStreamReader,並已新增向上轉換,讓這兩個分支都有 StreamReader 型別。 在 F# 6 中,這些向上轉換現在都會自動新增。 這表示程式碼變得更簡單:

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt")

您可以選擇性地啟用 /warnon:3388 警告,在每次使用其他隱含向上轉換時顯示警告,如隱含轉換的選擇性警告中所述。

隱含整數轉換

在 F# 6 中,當兩種型別都是已知型別時,32 位元整數會擴大為 64 位元整數。 例如,以典型的 API 圖形為例:

type Tensor(…) =
    static member Create(sizes: seq<int64>) = Tensor(…)

在 F# 5 中,必須使用 int64 的整數常值:

Tensor.Create([100L; 10L; 10L])

Tensor.Create([int64 100; int64 10; int64 10])

在 F# 6 中,當來源和目的地型別在型別推斷期間為已知時,就會自動進行 int32int64int32nativeint,以及 int32double 的擴展。 因此在上述範例的情況下,可以使用 int32 常值:

Tensor.Create([100; 10; 10])

儘管有這項變更,F# 在大部分情況下仍會繼續使用數值型別的明確擴展。 例如,當來源或目的地型別未知時,隱含擴展不適用於其他數值型別,例如 int8int16,或從 float32float64。 您也可以選擇性地啟用 /warnon:3389 警告,在每次使用隱含數值擴展時顯示警告,如隱含轉換的選擇性警告中所述。

.NET 樣式隱含轉換的頂級支援

在 F# 6 中,呼叫方法時,F# 程式碼中會自動套用 .NET “op_Implicit” 轉換。 例如,在 F# 5 中使用適用於 XML 的 .NET API 時,必須使用 XName.op_Implicit

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")

在 F# 6 中,當型別適用於來源運算式和目標型別時,就會針對引數運算式自動套用 op_Implicit 轉換:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")

您可以選擇性地啟用 /warnon:3395 警告,在方法引數上每次使用 op_Implicit 轉換擴展時顯示警告,如隱含轉換的選擇性警告中所述。

注意

在第一個 F# 6 版本中,此警告編號為 /warnon:3390。 由於發生衝突,警告編號已在後續版本中更新為 /warnon:3395

隱含轉換的選擇性警告

型別導向和隱含轉換可能難以與型別推斷互動,並導致難以理解的程式碼。 因此,有一些緩和措施可協助確保此功能不會在 F# 程式碼中濫用。 首先,來源和目的地型別都必須是明確已知的型別,而且不會有模棱兩可或額外的型別推斷。 其次,可以啟用選擇加入警告來報告任何隱含轉換的使用情況,並預設開啟一項警告:

  • /warnon:3388 (其他隱含向上轉換)
  • /warnon:3389 (隱含數值擴展)
  • /warnon:3391 (預設在非方法引數上開啟 op_Implicit)
  • /warnon:3395 (方法引數上的 op_Implicit)

如果您的小組想要禁止所有隱含轉換的使用,您也可以指定 /warnaserror:3388/warnaserror:3389/warnaserror:3391/warnaserror:3395

二進位數字的格式設定

F# 6 會將 %B 模式新增至二進位數字格式的可用格式指定名稱。 請試想下列 F# 程式碼:

printf "%o" 123
printf "%B" 123

此程式碼會列印下列輸出:

173
1111011

此功能會實作 F# RFC FS-1100

使用繫結時捨棄

F# 6 允許在 use 繫結中使用 _,例如:

let doSomething () =
    use _ = System.IO.File.OpenText("input.txt")
    printfn "reading the file"

此功能會實作 F# RFC FS-1102

InlineIfLambda

F# 編譯器包含執行內嵌程式碼的最佳化工具。 在 F# 6 中,我們新增了新的宣告式功能,可讓程式碼選擇性地指出,如果引數被判定為 Lambda 函式,則該引數本身應該一律內嵌在呼叫站台上。

例如,請考慮使用下列 iterateTwice 函式來周遊陣列:

let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
    for j = 0 to array.Length-1 do
        action array[j]
    for j = 0 to array.Length-1 do
        action array[j]

如果呼叫位置為:

let arr = [| 1.. 100 |]
let mutable sum = 0
arr  |> iterateTwice (fun x ->
    sum <- sum + x)

在進行內嵌和其他最佳化之後,程式碼會變成:

let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]

不同於舊版的 F#,不論涉及的 Lambda 運算式大小為何,都會套用此最佳化。 此功能也可以用來更可靠地實作迴圈展開及類似的轉換。

您可以開啟選擇加入警告 (/warnon:3517,預設為關閉),以指出程式碼中,InlineIfLambda 引數未繫結至呼叫站台上 Lambda 運算式的位置。 在正常情況下,不應該啟用此警告。 不過,在某些類型的高效能程式設計中,其有助於確保所有程式碼都已內嵌和壓平合併。

此功能會實作 F# RFC FS-1098

可繼續程式碼

F# 6 的 task {…} 支援建立在稱為可繼續程式碼RFC FS-1087 的基礎上。 可繼續程式碼是一項技術功能,可用來建置許多種高效能的非同步和狀態生成機器。

其他集合函式

FSharp.Core 6.0.0 將五個新的作業新增至核心集合函式。 這些函式為:

  • List/Array/Seq.insertAt
  • List/Array/Seq.removeAt
  • List/Array/Seq.updateAt
  • List/Array/Seq.insertManyAt
  • List/Array/Seq.removeManyAt

這些函式會在對應的集合型別或序列上執行複製和更新作業。 此型別的作業採「功能更新」形式。 如需這些函式的使用範例,請參閱對應文件,例如 List.insertAt

例如,請考慮在以 Elmish 樣式撰寫的簡單「待辦事項清單」應用程式上,使用模型、訊息和更新邏輯。 在這裡,使用者會與應用程式互動、產生訊息,而 update 函式會處理這些訊息,產生新的模型:

type Model =
    { ToDo: string list }

type Message =
    | InsertToDo of index: int * what: string
    | RemoveToDo of index: int
    | LoadedToDos of index: int * what: string list

let update (model: Model) (message: Message) =
    match message with
    | InsertToDo (index, what) ->
        { model with ToDo = model.ToDo |> List.insertAt index what }
    | RemoveToDo index ->
        { model with ToDo = model.ToDo |> List.removeAt index }
    | LoadedToDos (index, what) ->
        { model with ToDo = model.ToDo |> List.insertManyAt index what }

有了這些新的函式,邏輯將會清楚簡單,並只依賴不可變的資料。

此功能會實作 F# RFC FS-1113

對應具有索引鍵和值

在 FSharp.Core 6.0.0 中,Map 型別現在支援 KeysValues 屬性。 這些屬性不會複製基礎集合。

F# RFC FS-1113 中有此功能的描述。

NativePtr 的其他內建函式

FSharp.Core 6.0.0 會將新的內建函式新增至 NativePtr 模組:

  • NativePtr.nullPtr
  • NativePtr.isNullPtr
  • NativePtr.initBlock
  • NativePtr.clear
  • NativePtr.copy
  • NativePtr.copyBlock
  • NativePtr.ofILSigPtr
  • NativePtr.toILSigPtr

如同 NativePtr 中的其他函式,這些函式採內嵌形式,除非使用 /nowarn:9,否則其用法會發出警告。 這些函式的使用僅限於非受控型別。

F# RFC FS-1109 中有此功能的描述。

具有單位註釋的其他數數值型別

在 F# 6 中,下列型別或型別縮寫別名現在支援測量單位註釋。 新增項目會以粗體顯示:

F# 別名 CLR 型別
float32/single System.Single
float/double System.Double
decimal System.Decimal
sbyte/int8 System.SByte
int16 System.Int16
int/int32 System.Int32
int64 System.Int64
byte/uint8 System.Byte
uint16 System.UInt16
uint/uint32 System.UInt32
uint64 System.UIn64
nativeint System.IntPtr
unativeint System.UIntPtr

例如,您可以標註不帶正負號的整數,如下所示:

[<Measure>]
type days

let better_age = 3u<days>

F# RFC FS-1091 中有此功能的描述。

針對很少使用的符號運算子發出告知性警告

F# 6 新增軟式指引,針對 F# 6 和更新版本中的 :=!incrdecr 使用反正規化。 使用這些運算子和函式會產生告知性訊息,要求您明確使用 Value 屬性來取代程式碼。

在 F# 程式設計中,參考資料格可用於堆積配置的可變暫存器。 雖然參考資料格偶爾很有用,但新式 F# 程式碼中很少用到,因為可以改用 let mutable。 F# 核心程式庫包含與參考呼叫特別相關的兩個運算子::=! 及兩個函式:incrdecr。 這些運算子的存在讓參考資料格在 F# 程式設計中更重要,這讓所有 F# 程式設計人員都必須知道這些運算子。 此外,! 運算子很可能會與 C# 和其他語言中的 not 作業混淆,這可能是翻譯程式碼時的潛在錯誤來源。

這項變更的原理是減少 F# 程式設計人員需要知道的運算子數目,進而為初學者簡化 F#。

例如,試想下列 F# 5 程式碼:

let r = ref 0

let doSomething() =
    printfn "doing something"
    r := !r + 1

首先,新式 F# 程式碼中很少需要參考資料格,因此通常可以改為使用 let mutable

let mutable r = 0

let doSomething() =
    printfn "doing something"
    r <- r + 1

如果您使用參考資料格,F# 6 會發出告知性警告,要求您將最後一行變更為 r.Value <- r.Value + 1,並將您連結到適當使用參考資料格的進一步指引。

let r = ref 0

let doSomething() =
    printfn "doing something"
    r.Value <- r.Value + 1

這些訊息不是警告,而是 IDE 和編譯器輸出中顯示的「告知性訊息」。 F# 保持回溯相容。

此功能會實作 F# RFC FS-1111

F# 工具:.NET 6 預設用於在 Visual Studio 中編寫指令碼

如果您在 Visual Studio 中開啟或執行 F# 指令碼 (.fsx),預設會使用 .NET 6 搭配 64 位元執行來分析和執行指令碼。 這項功能在 Visual Studio 2019 的更新版本中處於預覽狀態,且現在預設為啟用。

若要啟用 .NET Framework 指令碼編寫,請選取 [工具]>[選項]>[F# 工具]>[F# 互動]。 將 [使用 .NET Core 指令碼編寫] 設定為 false,然後重新啟動 F# 互動視窗。 此設定會影響指令碼編輯和指令碼執行。 若要為 .NET Framework 指令碼編寫啟用 32 位元執行,也請將 [64 位元 F# 互動] 設定為 false。 .NET Core 指令碼編寫沒有 32 位元選項。

F# 工具:釘選 F# 指令碼的 SDK 版本

如果您使用 dotnet fsi 執行指令碼時所在的目錄包含具有 .NET SDK 設定的 global.json 檔案,則會使用列出的 .NET SDK 版本來執行和編輯指令碼。 此功能已在更新版本的 F# 5 中提供。

例如,假設指令碼所在目錄包含指定 .NET SDK 版本原則的 global.json 檔案:

{
  "sdk": {
    "version": "5.0.200",
    "rollForward": "minor"
  }
}

如果您現在在此目錄中使用 dotnet fsi 執行指令碼,則會使用該 SDK 版本。 這是一項強大的功能,可讓您「鎖定」用來編譯、分析及執行指令碼的 SDK。

如果您在 Visual Studio 和其他 IDE 中開啟和編輯指令碼,則工具會在分析和檢查指令碼時遵守此設定。 如果找不到 SDK,您必須在開發機器上安裝 SDK。

在 Linux 和其他 Unix 系統上,您可以將此與 shebang 結合,同時指定用於直接執行指令碼的語言版本。 適用於 script.fsx 的簡單 shebang 為:

#!/usr/bin/env -S dotnet fsi

printfn "Hello, world"

現在可以使用 script.fsx 直接執行指令碼。 您可以將此與特定的非預設語言版本結合,如下所示:

#!/usr/bin/env -S dotnet fsi --langversion:5.0

注意

編輯工具會忽略此設定,將分析認定為最新語言版本的指令碼。

移除舊版功能

自 F# 2.0 起,某些淘汰的舊版功能會有很長的指定警告。 除非您明確使用 /langversion:5.0,否則在 F# 6 中使用這些功能會產生錯誤。 產生錯誤的功能如下:

  • 使用後置型別名稱的多個泛型參數,例如 (int, int) Dictionary。 這在 F# 6 中會變成錯誤。 應該改用標準語法 Dictionary<int,int>
  • #indent "off". 這會變成錯誤。
  • x.(expr). 這會變成錯誤。
  • module M = struct … end . 這會變成錯誤。
  • 使用 *.ml*.mli 輸入。 這會變成錯誤。
  • 使用 (*IF-CAML*)(*IF-OCAML*)。 這會變成錯誤。
  • 使用 landlorlxorlsllsrasr 作為中置運算子。 這些是 F# 中的中置關鍵字,因為這些是 OCaml 中的中置關鍵字,而且未在 FSharp.Core 中定義。 使用這些關鍵字現在會發出警告。

這會實作 F# RFC FS-1114