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 中所述。
這項變更帶來三個優點:
- 需要較少的明確向上轉換
- 需要較少的明確整數轉換
- 新增 .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
在這裡,條件式分支會分別計算 TextReader
和 StreamReader
,並已新增向上轉換,讓這兩個分支都有 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 中,當來源和目的地型別在型別推斷期間為已知時,就會自動進行 int32
到 int64
、int32
到 nativeint
,以及 int32
到 double
的擴展。 因此在上述範例的情況下,可以使用 int32
常值:
Tensor.Create([100; 10; 10])
儘管有這項變更,F# 在大部分情況下仍會繼續使用數值型別的明確擴展。 例如,當來源或目的地型別未知時,隱含擴展不適用於其他數值型別,例如 int8
或 int16
,或從 float32
到 float64
。 您也可以選擇性地啟用 /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
型別現在支援 Keys 和 Values 屬性。 這些屬性不會複製基礎集合。
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 和更新版本中的 :=
、!
、incr
和 decr
使用反正規化。 使用這些運算子和函式會產生告知性訊息,要求您明確使用 Value
屬性來取代程式碼。
在 F# 程式設計中,參考資料格可用於堆積配置的可變暫存器。 雖然參考資料格偶爾很有用,但新式 F# 程式碼中很少用到,因為可以改用 let mutable
。 F# 核心程式庫包含與參考呼叫特別相關的兩個運算子::=
和 !
及兩個函式:incr
和 decr
。 這些運算子的存在讓參考資料格在 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*)
。 這會變成錯誤。 - 使用
land
、lor
、lxor
、lsl
、lsr
或asr
作為中置運算子。 這些是 F# 中的中置關鍵字,因為這些是 OCaml 中的中置關鍵字,而且未在 FSharp.Core 中定義。 使用這些關鍵字現在會發出警告。
這會實作 F# RFC FS-1114。