F# 6 中的新增功能
F# 6 增加了对 F# 语言和 F# 交互窗口的几项改进。 它随 .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
}
通过出色的 TaskBuilder.fs 和 Ply 库为 F# 5 提供了任务支持。 将代码迁移到内置支持应该非常简单。 但是,存在一些差异:内置支持与这些库之间的命名空间和类型推理略有不同,并且可能需要一些其他类型注释。 如有必要,你仍然可以在 F# 6 中使用这些社区库,只要你显式引用它们,并在每个文件中打开正确的命名空间。
使用 task {…}
与使用 async {…}
非常相似。 与 task {…}
相比,使用 async {…}
具有多个优点:
task {...}
的开销较低,可能会提高快速执行异步工作的热代码路径的性能。- 调试
task {…}
的单步执行和堆栈跟踪效果更好。 - 与需要或生成任务的 .NET 包进行互操作会更容易。
如果熟悉 async {…}
,请注意一些差异:
task {…}
立即将任务执行到第一个等待点。task {…}
不会隐式传播取消令牌。task {…}
不执行隐式取消检查。task {…}
不支持异步尾调用。 这意味着如果没有干预异步等待,则以递归方式使用return! ..
可能会导致堆栈溢出。
一般情况下,如果正在与使用任务的 .NET 库交互,并且不依赖于异步代码尾调用或隐式取消令牌传播,则应考虑在新代码中使用 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])
or
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 样式编写的简单“Todo List”应用程序的模型、消息和更新逻辑。 在这里,用户与应用程序交互,生成消息,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
操作混淆(这是翻译代码时不易察觉的潜在 bug 来源)。
此更改的基本原理是减少 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
),则默认情况下,该脚本会使用带 64 位执行的 .NET 6 进行分析和执行。 此功能在 Visual Studio 2019 的后续版本中以预览版提供,现在默认启用。
若要启用 .NET Framework 脚本,请选择“工具”>“选项”>“F# 工具”>“F# 交互窗口”。 将“使用 .NET Core 脚本”设置为“false”,然后重启“F# 交互窗口”窗口。 此设置会影响脚本编辑和脚本执行。 若要为 .NET Framework 脚本启用 32 位执行,还要将“64 位 F# 交互窗口”设置为“false”。 .NET Core 脚本没有 32 位选项。
F# 工具:固定 F# 脚本的 SDK 版本
如果在包含具有 .NET SDK 设置的 global.json 文件的目录中使用 dotnet fsi
执行脚本,则列出的 .NET SDK 版本将用于执行和编辑脚本。 此功能已在 F# 5 的更高版本中提供。
例如,假设目录中有一个脚本,其中包含下面指定 .NET SDK 版本策略的 global.json 文件:
{
"sdk": {
"version": "5.0.200",
"rollForward": "minor"
}
}
如果现在使用此目录中的 dotnet fsi
执行脚本,将遵循 SDK 版本。 这是一项强大的功能,可让你“锁定”用于编译、分析和执行脚本的 SDK。
如果在 Visual Studio 和其他 IDE 中打开和编辑脚本,该工具将在分析和检查脚本时遵循此设置。 如果找不到 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 起,一些弃用的旧功能一直在发出警告。 在 F# 6 中使用这些功能会导致错误,除非你显式使用 /langversion:5.0
。 指出错误的功能包括:
- 使用后缀类型名称的多个泛型参数,例如
(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。