F# 6 为 F# 语言和 F# 交互添加了多项改进。 它随 .NET 6 一起发布。
可以从 .NET 下载页下载最新的 .NET SDK。
开始
F# 6 在所有 .NET Core 分发版和 Visual Studio 工具中都可用。 有关详细信息,请参阅 F# 入门。
任务 {...}
F# 6 包括在 F# 代码中使用 任务表达式编写 .NET 任务的本机支持。 任务表达式类似于异步表达式,但允许你直接创作 .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# 的人的反复反馈,他们认为使用点表示法索引似乎是与标准行业实践不必要的偏离。
这不是重大更改,因为在默认情况下,使用expr.[idx]时不会发出任何警告。 但是,会发出一些建议代码澄清的信息性消息。 还可以选择激活进一步的信息性消息。 例如,您可以激活一个可选的信息性警告(/warnon:3366)以开始报告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。
可恢复代码
task {…} F# 6 的支持是基于称为可恢复代码RFC FS-1087 的基础构建的。 可恢复代码是一项技术功能,可用于生成多种高性能异步和生成状态机。
其他集合函数
FSharp.Core 6.0.0 向核心集合函数增加了五个新的操作。 这些函数包括:
- List/Array/Seq.insertAt
- List/Array/Seq.removeAt
- 列表/数组/序列.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 类型现在支持 键 和 值 属性。 这些属性不复制基础集合。
此功能记录在 F# RFC FS-1113 中。
NativePtr 的其他内部函数
FSharp.Core 6.0.0 向 NativePtr 模块添加新的内部函数:
NativePtr.nullPtrNativePtr.isNullPtrNativePtr.initBlockNativePtr.clearNativePtr.copyNativePtr.copyBlockNativePtr.ofILSigPtrNativePtr.toILSigPtr
与其他函数一样,这些函数是内联的,除非使用/nowarn:9,否则它们的使用会发出警告NativePtr。 这些函数的使用仅限于非托管类型。
此功能记录在 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),则默认情况下,将使用 .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 版本
如果在包含具有 .NET SDK 设置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 是:
#!/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# 中的 infix 关键字,因为它们是 OCaml 中的 infix 关键字,并且未在 FSharp.Core 中定义。 使用这些关键字现在将发出警告。
这实现了 F# RFC FS-1114。