计算表达式

F# 中的计算表达式提供了一种方便的语法,用于编写可以使用控制流构造和绑定进行排序和组合的计算。 根据计算表达式类型,可以认为它们是表达 monads、monoid、monad 转换器和适用性函数的一种方法。 但是,与其他语言 (如 Haskell ) 中的表示法)不同,它们不绑定到单个抽象,也不依赖于宏或其他形式的元编程来实现方便且上下文相关的语法。

概述

计算可以有多种形式。 最常见的计算形式是单线程执行,易于理解和修改。 但是,并非所有形式的计算都像单线程执行一样简单。 示例包括:

  • 非确定性计算
  • 异步计算
  • 有效计算
  • 生成计算

更一 般而言,必须在 应用程序的某些部分执行上下文相关计算。 编写上下文相关的代码可能比较困难,因为很容易在给定上下文之外"泄漏"计算,而无需抽象来防止你这样做。 这些抽象通常难以自己编写,正因如此,F# 有一种通用的方式来执行所谓的计算 表达式

计算表达式为编码上下文相关计算提供统一的语法和抽象模型。

每个计算表达式都由生成器 类型 支持。 生成器类型定义可用于计算表达式的操作。 请参阅 创建新的计算表达式类型,它演示如何创建自定义计算表达式。

语法概述

所有计算表达式都具有以下形式:

builder-expr { cexper }

在此窗体中, 是定义计算表达式的生成器类型的名称,是计算 builder-expr cexper 表达式的表达式主体。 例如, async 计算表达式代码可能如下所示:

let fetchAndDownload url =
    async {
        let! data = downloadData url

        let processedData = processData data

        return processedData
    }

计算表达式中提供了一个特殊的附加语法,如前面的示例所示。 以下表达式形式可以与计算表达式一起使用:

expr { let! ... }
expr { do! ... }
expr { yield ... }
expr { yield! ... }
expr { return ... }
expr { return! ... }
expr { match! ... }

每个关键字和其他标准 F# 关键字仅在计算表达式中可用(如果它们已在后备生成器类型中定义)。 唯一的例外是 ,它本身是语法上的,用于使用 ,后 match! let! 跟结果的模式匹配。

生成器类型是一个对象,用于定义控制计算表达式片段组合方式的特殊方法;也就是说,它的方法控制计算表达式的行为方式。 描述生成器类的另一种方式是,它使你能够自定义许多 F# 构造(如循环和绑定)的操作。

let!

let!关键字将调用另一个计算表达式的结果绑定到名称:

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        ...
    }

如果使用 将调用绑定到计算表达式 let ,则将不会获取计算表达式的结果。 相反,你将未 实现调用的值 绑定到该计算表达式。 使用 let! 绑定到结果。

let! 由生成器 Bind(x, f) 类型上的 成员定义。

do!

do!关键字用于调用一个计算表达式,该表达式返回由生成器 (成员定义的类似 类型的 unit Zero) :

let doThingsAsync data url =
    async {
        do! submitData data url
        ...
    }

对于 异步工作流,此类型为 Async<unit> 。 对于其他计算表达式,类型可能为 CExpType<unit>

do! 由生成器 Bind(x, f) 类型上的 成员定义,其中 f 生成 unit

yield

yield关键字用于从计算表达式返回值,以便它可以用作 IEnumerable<T>

let squares =
    seq {
        for i in 1..10 do
            yield i * i
    }

for sq in squares do
    printfn $"%d{sq}"

在大多数情况下,调用方可以省略它。 省略的最常见方式 yield 是使用 -> 运算符:

let squares =
    seq {
        for i in 1..10 -> i * i
    }

for sq in squares do
    printfn $"%d{sq}"

对于可能生成许多不同值的更复杂的表达式(也许有条件地)只需省略 关键字就可以:

let weekdays includeWeekend =
    seq {
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        if includeWeekend then
            "Saturday"
            "Sunday"
    }

C# 中的 yield 关键字一样,计算表达式中每个元素在被重新使用时将返回。

yield 由生成器 Yield(x) 类型上的成员定义, x 其中 是要返回的项。

yield!

yield!关键字用于平展计算表达式中的值集合:

let squares =
    seq {
        for i in 1..3 -> i * i
    }

let cubes =
    seq {
        for i in 1..3 -> i * i * i
    }

let squaresAndCubes =
    seq {
        yield! squares
        yield! cubes
    }

printfn $"{squaresAndCubes}"  // Prints - 1; 4; 9; 1; 8; 27

计算时,由 调用的计算表达式将一个一个地生成其项, yield! 并平展结果。

yield! 由生成器 YieldFrom(x) 类型上的成员定义,其中 x 是值的集合。

yield 不同 yield! ,必须显式指定 。 其行为在计算表达式中不是隐式的。

return

return关键字将值包装在与计算表达式对应的类型中。 除了使用 的计算表达式 yield 之外,它还用于"完成"计算表达式:

let req = // 'req' is of type 'Async<data>'
    async {
        let! data = fetch url
        return data
    }

// 'result' is of type 'data'
let result = Async.RunSynchronously req

return 由生成器 Return(x) 类型上的成员定义, x 其中 是要包装的项。

return!

return!关键字实现计算表达式的值,并包装结果为与计算表达式对应的类型:

let req = // 'req' is of type 'Async<data>'
    async {
        return! fetch url
    }

// 'result' is of type 'data'
let result = Async.RunSynchronously req

return! 由生成器 ReturnFrom(x) 类型上的 成员定义,其中 x 是另一个计算表达式。

match!

关键字 match! 允许你对另一个计算表达式的调用内联,并在其结果上设置模式匹配:

let doThingsAsync url =
    async {
        match! callService url with
        | Some data -> ...
        | None -> ...
    }

使用 调用计算表达式 match! 时,它将实现类似 的调用结果 let! 。 在调用计算表达式(其中结果为可选 )时,通常会 使用此方法

内置计算表达式

F# 核心库定义了四个内置计算表达式:序列表达式异步表达式任务表达式查询表达式

创建新类型的计算表达式

可以通过创建生成器类并定义类上的某些特殊方法来定义自己的计算表达式的特征。 生成器类可以选择定义下表中列出的方法。

下表描述了可在工作流生成器类中使用的方法。

方法 典型 (签名) 说明
Bind M<'T> * ('T -> M<'U>) -> M<'U> 在计算 let! 表达式 do! 中为 和 调用。
Delay (unit -> M<'T>) -> Delayed<'T> 将计算表达式包装为函数。 Delayed<'T> 可以是任意类型,通常使用 M<'T> unit -> M<'T> 或 。 默认实现返回 M<'T>
Return 'T -> M<'T> 在计算 return 表达式中为 调用。
ReturnFrom M<'T> -> M<'T> 在计算 return! 表达式中为 调用。
Run Delayed<'T> -> M<'T>

M<'T> -> 'T
执行计算表达式。
Combine M<'T> * Delayed<'T> -> M<'T>

M<unit> * M<'T> -> M<'T>
为计算表达式中的序列化调用。
For seq<'T> * ('T -> M<'U>) -> M<'U>

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
为计算 for...do 表达式中的表达式调用。
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> 为计算 try...finally 表达式中的表达式调用。
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> 为计算 try...with 表达式中的表达式调用。
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable use 计算表达式中的绑定调用。
While (unit -> bool) * Delayed<'T> -> M<'T>

(unit -> bool) * Delayed<unit> -> M<unit>
为计算 while...do 表达式中的表达式调用。
Yield 'T -> M<'T> 为计算 yield 表达式中的表达式调用。
YieldFrom M<'T> -> M<'T> 为计算 yield! 表达式中的表达式调用。
Zero unit -> M<'T> 为计算 else 表达式中的 if...then 表达式的空分支调用。
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> 指示计算表达式作为引号 Run 传递给成员。 它将计算的所有实例转换为引号。

生成器类中的许多方法都使用 并返回 构造,该构造通常是一种单独定义的类型,用于描述要组合的计算类型,例如,用于异步表达式和 M<'T> Async<'T> Seq<'T> 序列工作流。 这些方法的签名使它们能够相互组合和嵌套,以便可以将从一个构造返回的工作流对象传递到下一个构造。

许多函数使用 的结果 Delay 作为参数 Run While :、、、 TryWith TryFinallyCombineDelayed<'T>类型是 的返回类型 Delay ,因此也是这些函数的参数。 Delayed<'T> 可以是不需要与 相关的任意类型; M<'T> 通常 M<'T> 使用 或 (unit -> M<'T>) 。 默认实现为 M<'T> 。 有关 更深入的 了解,请参阅此处。

编译器在分析计算表达式时,使用上表中的方法和计算表达式中的代码,将表达式转换为一系列嵌套函数调用。 嵌套表达式的形式如下:

builder.Run(builder.Delay(fun () -> {| cexpr |}))

在上面的代码中, Run Delay 如果未在计算表达式生成器类中定义,则将忽略对和的调用。 下面的表中所述的翻译将计算表达式的主体(此处表示为 {| cexpr |} )转换为涉及生成器类方法的调用。 {| cexpr |}根据这些转换以递归方式定义计算表达式,其中 expr 是 F # 表达式, cexpr 是计算表达式。

Expression 翻译
{ let binding in cexpr } let binding in {| cexpr |}
{ let! pattern = expr in cexpr } builder.Bind(expr, (fun pattern -> {| cexpr |}))
{ do! expr in cexpr } builder.Bind(expr, (fun () -> {| cexpr |}))
{ yield expr } builder.Yield(expr)
{ yield! expr } builder.YieldFrom(expr)
{ return expr } builder.Return(expr)
{ return! expr } builder.ReturnFrom(expr)
{ use pattern = expr in cexpr } builder.Using(expr, (fun pattern -> {| cexpr |}))
{ use! value = expr in cexpr } builder.Bind(expr, (fun value -> builder.Using(value, (fun value -> { cexpr }))))
{ if expr then cexpr0 |} if expr then { cexpr0 } else builder.Zero()
{ if expr then cexpr0 else cexpr1 |} if expr then { cexpr0 } else { cexpr1 }
{ match expr with | pattern_i -> cexpr_i } match expr with | pattern_i -> { cexpr_i }
{ for pattern in expr do cexpr } builder.For(enumeration, (fun pattern -> { cexpr }))
{ for identifier = expr1 to expr2 do cexpr } builder.For(enumeration, (fun identifier -> { cexpr }))
{ while expr do cexpr } builder.While(fun () -> expr, builder.Delay({ cexpr }))
{ try cexpr with | pattern_i -> expr_i } builder.TryWith(builder.Delay({ cexpr }), (fun value -> match value with | pattern_i -> expr_i | exn -> System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exn).Throw())))
{ try cexpr finally expr } builder.TryFinally(builder.Delay( { cexpr }), (fun () -> expr))
{ cexpr1; cexpr2 } builder.Combine({ cexpr1 }, { cexpr2 })
{ other-expr; cexpr } expr; { cexpr }
{ other-expr } expr; builder.Zero()

在上表中, other-expr 描述了表中未列出的表达式。 生成器类不需要实现所有方法并支持上表中列出的所有翻译。 未实现的这些构造在该类型的计算表达式中不可用。 例如,如果不想 use 在计算表达式中支持关键字,则可以 Use 在生成器类中省略的定义。

下面的代码示例演示一个计算表达式,该表达式将计算封装为一系列步骤,一次只能对一个步骤进行计算。 一种可区分联合类型,用于 OkOrException 对迄今为止计算的表达式的错误状态进行编码。 此代码演示了几种可在计算表达式中使用的典型模式,如某些生成器方法的样本实现。

/// Represents computations that can be run step by step
type Eventually<'T> =
    | Done of 'T
    | NotYetDone of (unit -> Eventually<'T>)

module Eventually =

    /// Bind a computation using 'func'.
    let rec bind func expr =
        match expr with
        | Done value -> func value
        | NotYetDone work -> NotYetDone (fun () -> bind func (work()))

    /// Return the final value
    let result value = Done value

    /// The catch for the computations. Stitch try/with throughout
    /// the computation, and return the overall result as an OkOrException.
    let rec catch expr =
        match expr with
        | Done value -> result (Ok value)
        | NotYetDone work ->
            NotYetDone (fun () ->
                let res = try Ok(work()) with | exn -> Exception exn
                match res with
                | Ok cont -> catch cont // note, a tailcall
                | Error exn -> result (Error exn))

    /// The delay operator.
    let delay func = NotYetDone (fun () -> func())

    /// The stepping action for the computations.
    let step expr =
        match expr with
        | Done _ -> expr
        | NotYetDone func -> func ()

    /// The tryFinally operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryFinally expr compensation =
        catch (expr)
        |> bind (fun res ->
            compensation();
            match res with
            | Ok value -> result value
            | Error exn -> raise exn)

    /// The tryWith operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryWith exn handler =
        catch exn
        |> bind (function Ok value -> result value | Error exn -> handler exn)

    /// The whileLoop operator.
    /// This is boilerplate in terms of "result" and "bind".
    let rec whileLoop pred body =
        if pred() then body |> bind (fun _ -> whileLoop pred body)
        else result ()

    /// The sequential composition operator.
    /// This is boilerplate in terms of "result" and "bind".
    let combine expr1 expr2 =
        expr1 |> bind (fun () -> expr2)

    /// The using operator.
    /// This is boilerplate in terms of "tryFinally" and "Dispose".
    let using (resource: #System.IDisposable) func =
        tryFinally (func resource) (fun () -> resource.Dispose())

    /// The forLoop operator.
    /// This is boilerplate in terms of "catch", "result", and "bind".
    let forLoop (collection:seq<_>) func =
        let ie = collection.GetEnumerator()
        tryFinally
            (whileLoop
                (fun () -> ie.MoveNext())
                (delay (fun () -> let value = ie.Current in func value)))
            (fun () -> ie.Dispose())

/// The builder class.
type EventuallyBuilder() =
    member x.Bind(comp, func) = Eventually.bind func comp
    member x.Return(value) = Eventually.result value
    member x.ReturnFrom(value) = value
    member x.Combine(expr1, expr2) = Eventually.combine expr1 expr2
    member x.Delay(func) = Eventually.delay func
    member x.Zero() = Eventually.result ()
    member x.TryWith(expr, handler) = Eventually.tryWith expr handler
    member x.TryFinally(expr, compensation) = Eventually.tryFinally expr compensation
    member x.For(coll:seq<_>, func) = Eventually.forLoop coll func
    member x.Using(resource, expr) = Eventually.using resource expr

let eventually = new EventuallyBuilder()

let comp =
    eventually {
        for x in 1..2 do
            printfn $" x = %d{x}"
        return 3 + 4
    }

/// Try the remaining lines in F# interactive to see how this
/// computation expression works in practice.
let step x = Eventually.step x

// returns "NotYetDone <closure>"
comp |> step

// prints "x = 1"
// returns "NotYetDone <closure>"
comp |> step |> step

// prints "x = 1"
// prints "x = 2"
// returns "Done 7"
comp |> step |> step |> step |> step

计算表达式具有表达式返回的基础类型。 基础类型可能表示可以执行的计算结果或延迟计算,或者它可能提供一种方法来循环访问某些类型的集合。 在上面的示例中,基础类型是 Eventually<_> 。 对于序列表达式,基础类型是 System.Collections.Generic.IEnumerable<T> 。 对于查询表达式,基础类型是 System.Linq.IQueryable 。 对于异步表达式,基础类型是 AsyncAsync对象表示要执行的工作来计算结果。 例如,调用 Async.RunSynchronously 以执行计算并返回结果。

自定义操作

可以在计算表达式中定义自定义操作,并使用自定义操作作为计算表达式中的运算符。 例如,可以在查询表达式中包含查询运算符。 定义自定义操作时,必须在计算表达式中定义 Yield 和方法。 若要定义自定义操作,请将其放在计算表达式的 builder 类中,然后应用 CustomOperationAttribute 。 此属性采用字符串作为参数,该参数是要在自定义操作中使用的名称。 此名称将出现在计算表达式的左大括号开头的范围内。 因此,不应使用与此块中的自定义操作名称相同的标识符。 例如,避免在 all 查询表达式中使用标识符(如或) last

利用新的自定义操作扩展现有生成器

如果已经有一个生成器类,则可以从该生成器类的外部扩展其自定义操作。 扩展必须在模块中声明。 命名空间不能包含在定义该类型的同一文件和相同的命名空间声明组中的扩展成员。

下面的示例演示了现有类的扩展 FSharp.Linq.QueryBuilder

open System
open FSharp.Linq

type QueryBuilder with

    [<CustomOperation("existsNot")>]
    member _.ExistsNot (source: QuerySource<'T, 'Q>, predicate) =
        System.Linq.Enumerable.Any (source.Source, Func<_,_>(predicate)) |> not

可以重载自定义操作。 有关详细信息,请参阅 F # RFC FS-1056-允许在计算表达式中重载自定义关键字

有效编译计算表达式

通过仔细使用称为可恢复 代码 的低级功能,可以将挂起执行的 F # 计算表达式编译为高效的状态机。 可恢复代码记录在 F # RFC FS-1087 中,用于 任务表达式

作为同步 (的 F # 计算表达式即,它们不会挂起执行) 也可以通过使用 内联函数 (包括属性)将其编译为高效状态机 InlineIfLambdaF # RFC FS-1098中提供了示例。

F # 编译器向列表表达式、数组表达式和序列表达式提供特殊处理,以确保生成高性能代码。

另请参阅