F# 5 中的新增功能

F# 5 增加了对 F# 语言和 F# 交互窗口的几项改进。 它随 .NET 5 一起发布。

可以从 .NET 下载页下载最新 .NET SDK。

入门

F# 5 在所有 .NET Core 分发版和 Visual Studio 工具中提供。 有关详细信息,请参阅 F# 入门以了解更多信息。

F# 脚本中的包引用

F# 5 支持使用 #r "nuget:..." 语法在 F# 脚本中进行包引用。 例如,请考虑以下包引用:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let o = {| X = 2; Y = "Hello" |}

printfn $"{JsonConvert.SerializeObject o}"

还可以在包的名称后提供显式版本,如下所示:

#r "nuget: Newtonsoft.Json,11.0.1"

包引用支持具有本机依赖项的包,例如 ML.NET。

包引用还支持对引用依赖 .dll 具有特殊要求的包。 例如,FParsec 包用于要求用户手动确保先引用其依赖 FParsecCS.dll,然后再在 F# 交互窗口中引用 FParsec.dll。 不再需要这样,可以按如下所示引用该包:

#r "nuget: FParsec"

open FParsec

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn $"Success: {result}"
    | Failure(errorMsg, _, _) -> printfn $"Failure: {errorMsg}"

test pfloat "1.234"

此功能实现 F# 工具 RFC FST-1027。 有关包引用详细信息,请参阅 F# 交互窗口教程。

字符串内插

F# 内插字符串与 C# 或 JavaScript 内插字符串非常相似,因为它们允许你可以在字符串文本内的“孔”中编写代码。 下面是一个基本示例:

let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"

printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"

但是,F# 内插字符串还允许类型化内插(就如同 sprintf 函数一样),以强制内插上下文内的表达式符合特定类型。 它使用相同的格式说明符。

let name = "Phillip"
let age = 29

printfn $"Name: %s{name}, Age: %d{age}"

// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"

在前面的类型化内插示例中,%s 要求内插属于类型 string,而 %d 要求内插为 integer

此外,任何任意的 F# 表达式(或多个表达式)都可以置于内插上下文的一侧。 甚至可以编写更复杂的表达式,如下所示:

let str =
    $"""The result of squaring each odd item in {[1..10]} is:
{
    let square x = x * x
    let isOdd x = x % 2 <> 0
    let oddSquares xs =
        xs
        |> List.filter isOdd
        |> List.map square
    oddSquares [1..10]
}
"""

不过在实践中不建议执行太多这种操作。

此功能实现 F# RFC FS-1001

支持 nameof

F# 5 支持 nameof 运算符,该运算符会解析用于它的符号,并在 F# 源中生成其名称。 这可用于各种方案(如日志记录)并保护日志记录免受源代码更改的影响。

let months =
    [
        "January"; "February"; "March"; "April";
        "May"; "June"; "July"; "August"; "September";
        "October"; "November"; "December"
    ]

let lookupMonth month =
    if (month > 12 || month < 1) then
        invalidArg (nameof month) (sprintf "Value passed in was %d." month)

    months[month-1]

printfn $"{lookupMonth 12}"
printfn $"{lookupMonth 1}"
printfn $"{lookupMonth 13}"

最后一行会引发异常,错误消息中会显示“月份”。

可以采用几乎每个 F# 构造的名称:

module M =
    let f x = nameof x

printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"

最后添加的三个内容是对运算符工作方式的更改:添加泛型类型参数的 nameof<'type-parameter> 形式,以及能够在模式匹配表达式中使用 nameof 作为模式。

采用运算符的名称可提供其源字符串。 如果需要已编译形式,请使用运算符的已编译名称:

nameof(+) // "+"
nameof op_Addition // "op_Addition"

采用类型参数的名称需要略有不同的语法:

type C<'TType> =
    member _.TypeName = nameof<'TType>

这类似于 typeof<'T>typedefof<'T> 运算符。

F# 5 还添加了对可在 match 表达式中使用的 nameof 模式的支持:

[<Struct; IsByRefLike>]
type RecordedEvent = { EventType: string; Data: ReadOnlySpan<byte> }

type MyEvent =
    | AData of int
    | BData of string

let deserialize (e: RecordedEvent) : MyEvent =
    match e.EventType with
    | nameof AData -> AData (JsonSerializer.Deserialize<int> e.Data)
    | nameof BData -> BData (JsonSerializer.Deserialize<string> e.Data)
    | t -> failwithf "Invalid EventType: %s" t

前面的代码在匹配表达式中使用“nameof”而不是字符串文本。

此功能实现 F# RFC FS-1003

开放类型声明

F# 5 还添加了对开放类型声明的支持。 开放类型声明如同在 C# 中打开静态类,只是使用了一些不同的语法和一些稍微不同的行为来适应 F# 语义。

借助开放类型声明,可以对任何类型执行 open 操作以公开其中的静态内容。 此外,可以对 F# 定义的联合和记录执行 open 操作以公开其内容。 例如,如果在模块中定义了联合,并且要访问其用例,但不想打开整个模块,则这可能会非常有用。

open type System.Math

let x = Min(1.0, 2.0)

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn $"{A}"

与 C# 不同,对公开同名的成员的两个类型执行 open type 操作时,进行 open 的最后一个类型中的成员会隐藏另一个名称。 这与围绕隐藏的已存在 F# 语义一致。

此功能实现 F# RFC FS-1068

内置数据类型的一致切片行为

对内置 FSharp.Core 数据类型(数组、列表、字符串、2D 数组、3D 数组、4D 数组)进行切片的行为在 F# 5 之前曾经不一致。 某些边缘用例行为会引发异常,而另一些则不会引发异常。 在 F# 5 中,所有内置类型现在都为无法生成的切片返回空切片:

let l = [ 1..10 ]
let a = [| 1..10 |]
let s = "hello!"

// Before: would return empty list
// F# 5: same
let emptyList = l[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty array
let emptyArray = a[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty string
let emptyString = s[-2..(-1)]

此功能实现 F# RFC FS-1077

FSharp.Core 中 3D 和 4D 数组的固定索引切片

F# 5 支持内置 3D 和 4D 数组类型中具有固定索引的切片。

为了说明这一点,请参考以下 3D 数组:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

如果要从数组中提取切片 [| 4; 5 |],该怎么办? 现在这非常简单!

// First, create a 3D array to slice

let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable count = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m[x,y,z] <- count
            count <- count + 1

// Now let's get the [4;5] slice!
m[*, 0, 1]

此功能实现 F# RFC FS-1077b

F# 引用改进

F# 代码引用现在能够保留类型约束信息。 请考虑以下示例:

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

inline 函数生成的约束保留在代码引用中。 现在可以计算 negate 函数的引用形式。

此功能实现 F# RFC FS-1071

适用的计算表达式

计算表达式 (CE) 现在用于对“上下文计算”(如果采用更适合于函数编程的术语,则是一元计算)进行建模。

F# 5 引入了适用的 CE,它们可提供不同的计算模型。 适用的 CE 可实现更高效的计算,前提是每次计算都是独立的,并且其结果会在最后累积。 当计算彼此独立时,它们也很容易并行化,从而使 CE 创建者能够编写更高效的库。 不过此优势有一个限制:不允许使用依赖于以前计算的值的计算。

下面的示例演示 Result 类型的基本适用的 CE。

// First, define a 'zip' function
module Result =
    let zip x1 x2 =
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }

    match res1 with
    | Ok x -> printfn $"{nameof res1} is: %d{x}"
    | Error e -> printfn $"{nameof res1} is: {e}"

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

如果你是目前在库中公开 CE 的库创建者,则还需要了解一些额外的注意事项。

此功能实现 F# RFC FS-1063

接口可以在不同的泛型实例化中实现

现在可以在不同的泛型实例化中实现相同的接口:

type IA<'T> =
    abstract member Get : unit -> 'T

type MyClass() =
    interface IA<int> with
        member x.Get() = 1
    interface IA<string> with
        member x.Get() = "hello"

let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>

iaInt.Get() // 1
iaString.Get() // "hello"

此功能实现 F# RFC FS-1031

默认接口成员消耗

F# 5 允许使用具有默认实现的接口

请考虑 C# 中定义的接口,如下所示:

using System;

namespace CSharp
{
    public interface MyDim
    {
        public int Z => 0;
    }
}

可以通过实现接口的任何标准方式在 F# 中使用它:

open CSharp

// You can implement the interface via a class
type MyType() =
    member _.M() = ()

    interface MyDim

let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"

// You can also implement it via an object expression
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"

这使你能够在期望用户能够使用默认实现时,安全地利用采用现代 C# 编写的 C# 代码和 .NET 组件。

此功能实现 F# RFC FS-1074

与可以为 null 的值类型的简化互操作

可以为 null 的(值)类型(历史上称为可以为 null 的类型)长期以来一直受 F# 支持,但与它们交互在传统上有点困难,因为每次要传递值时都必须构造 NullableNullable<SomeType> 包装器。 现在,如果目标类型匹配,则编译器会将值类型隐式转换为 Nullable<ThatValueType>。 现在可以执行以下代码:

#r "nuget: Microsoft.Data.Analysis"

open Microsoft.Data.Analysis

let dateTimes = PrimitiveDataFrameColumn<DateTime>("DateTimes")

// The following line used to fail to compile
dateTimes.Append(DateTime.Parse("2019/01/01"))

// The previous line is now equivalent to this line
dateTimes.Append(Nullable<DateTime>(DateTime.Parse("2019/01/01")))

此功能实现 F# RFC FS-1075

预览:反向索引

F# 5 还引入了允许反向索引的预览。 语法为 ^idx。 下面介绍了如何从列表末尾查找元素 1 值:

let xs = [1..10]

// Get element 1 from the end:
xs[^1]

// From the end slices

let lastTwoOldStyle = xs[(xs.Length-2)..]

let lastTwoNewStyle = xs[^1..]

lastTwoOldStyle = lastTwoNewStyle // true

还可以为自己的类型定义反向索引。 为此,需要实现以下方法:

GetReverseIndex: dimension: int -> offset: int

下面是 Span<'T> 类型的示例:

open System

type Span<'T> with
    member sp.GetSlice(startIdx, endIdx) =
        let s = defaultArg startIdx 0
        let e = defaultArg endIdx sp.Length
        sp.Slice(s, e - s)

    member sp.GetReverseIndex(_, offset: int) =
        sp.Length - offset

let printSpan (sp: Span<int>) =
    let arr = sp.ToArray()
    printfn $"{arr}"

let run () =
    let sp = [| 1; 2; 3; 4; 5 |].AsSpan()

    // Pre-# 5.0 slicing on a Span<'T>
    printSpan sp[0..] // [|1; 2; 3; 4; 5|]
    printSpan sp[..3] // [|1; 2; 3|]
    printSpan sp[1..3] // |2; 3|]

    // Same slices, but only using from-the-end index
    printSpan sp[..^0] // [|1; 2; 3; 4; 5|]
    printSpan sp[..^2] // [|1; 2; 3|]
    printSpan sp[^4..^2] // [|2; 3|]

run() // Prints the same thing twice

此功能实现 F# RFC FS-1076

预览:计算表达式中的自定义关键字的重载

计算表达式是适用于库和框架创建者的强大功能。 它们允许定义已知成员并为正在其中工作的域形成 DSL,从而可极大地提高组件的表现力。

F# 5 为在计算表达式中重载自定义操作添加了预览支持。 它允许编写和使用以下代码:

open System

type InputKind =
    | Text of placeholder:string option
    | Password of placeholder: string option

type InputOptions =
  { Label: string option
    Kind : InputKind
    Validators : (string -> bool) array }

type InputBuilder() =
    member t.Yield(_) =
      { Label = None
        Kind = Text None
        Validators = [||] }

    [<CustomOperation("text")>]
    member this.Text(io, ?placeholder) =
        { io with Kind = Text placeholder }

    [<CustomOperation("password")>]
    member this.Password(io, ?placeholder) =
        { io with Kind = Password placeholder }

    [<CustomOperation("label")>]
    member this.Label(io, label) =
        { io with Label = Some label }

    [<CustomOperation("with_validators")>]
    member this.Validators(io, [<ParamArray>] validators) =
        { io with Validators = validators }

let input = InputBuilder()

let name =
    input {
    label "Name"
    text
    with_validators
        (String.IsNullOrWhiteSpace >> not)
    }

let email =
    input {
    label "Email"
    text "Your email"
    with_validators
        (String.IsNullOrWhiteSpace >> not)
        (fun s -> s.Contains "@")
    }

let password =
    input {
    label "Password"
    password "Must contains at least 6 characters, one number and one uppercase"
    with_validators
        (String.exists Char.IsUpper)
        (String.exists Char.IsDigit)
        (fun s -> s.Length >= 6)
    }

在进行此更改之前,可以按原样编写 InputBuilder 类型,但无法采用在示例中使用它的方式来进行使用。 由于现在允许使用重载、可选参数和 System.ParamArray 类型,因此一切都按预期方式工作。

此功能实现 F# RFC FS-1056