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 中所述。

此更改有三个优点:

  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])

or

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 样式编写的简单“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 类型现在支持 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 操作混淆(这是翻译代码时不易察觉的潜在 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*)。 这会成为一个错误。
  • landlorlxorlsllsrasr 作为中缀运算符。 这些是 F# 中的中缀关键字,因为它们是 OCaml 中的中缀关键字,而不是在 FSharp.core 中定义。 现在使用这些关键字将发出警告。

此功能实现 F# RFC FS-1114