计算表达式Computation Expressions

中F#的计算表达式提供了一种方便的语法,用于编写可使用控制流结构和绑定进行序列化和组合的计算。Computation expressions in F# provide a convenient syntax for writing computations that can be sequenced and combined using control flow constructs and bindings. 根据计算表达式的类型,可以将其视为表示 monad、monoids、monad 转换器和 applicative 函子的一种方法。Depending on the kind of computation expression, they can be thought of as a way to express monads, monoids, monad transformers, and applicative functors. 但是,与其他语言(如 Haskell 中的 -notation )不同,它们不会与单个抽象相关,也不依赖于宏或其他形式的元编程来实现方便且区分上下文的语法。However, unlike other languages (such as do-notation in Haskell), they are not tied to a single abstraction, and do not rely on macros or other forms of metaprogramming to accomplish a convenient and context-sensitive syntax.

概述Overview

计算可以采用多种形式。Computations can take many forms. 最常见的计算形式是单线程执行,它易于理解和修改。The most common form of computation is single-threaded execution, which is easy to understand and modify. 但是,并非所有形式的计算都像单线程执行一样简单。However, not all forms of computation are as straightforward as single-threaded execution. 一些示例包括:Some examples include:

  • 非确定性计算Non-deterministic computations
  • 异步计算Asynchronous computations
  • Effectful 计算Effectful computations
  • 生成计算Generative computations

通常,在应用程序的某些部分,您必须执行上下文相关的计算。More generally, there are context-sensitive computations that you must perform in certain parts of an application. 编写上下文相关的代码可能会很难,因为在没有抽象的情况下,可以轻松地在给定上下文之外 "泄漏" 计算,以防您这样做。Writing context-sensitive code can be challenging, as it is easy to "leak" computations outside of a given context without abstractions to prevent you from doing so. 这些抽象通常很难自行编写,这就是F#一种通用的方法来实现这种计算表达式These abstractions are often challenging to write by yourself, which is why F# has a generalized way to do so called computation expressions.

计算表达式为区分上下文的计算提供统一的语法和抽象模型。Computation expressions offer a uniform syntax and abstraction model for encoding context-sensitive computations.

每个计算表达式都是由生成器类型支持的。Every computation expression is backed by a builder type. 生成器类型定义可用于计算表达式的操作。The builder type defines the operations that are available for the computation expression. 请参阅创建新类型的计算表达式,其中显示了如何创建自定义计算表达式。See Creating a New Type of Computation Expression, which shows how to create a custom computation expression.

语法概述Syntax overview

所有计算表达式具有以下形式:All computation expressions have the following form:

builder-expr { cexper }

其中 builder-expr 是定义计算表达式的生成器类型的名称,而 cexper 是计算表达式的表达式主体。where builder-expr is the name of a builder type that defines the computation expression, and cexper is the expression body of the computation expression. 例如,async 计算表达式代码可能如下所示:For example, async computation expression code can look like this:

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

        let processedData = processData data

        return processedData
    }

计算表达式中有一种特殊的附加语法,如前面的示例所示。There is a special, additional syntax available within a computation expression, as shown in the previous example. 以下表达式窗体可用于计算表达式:The following expression forms are possible with computation expressions:

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

其中的每个关键字和其他标准F#关键字仅在计算表达式中可用(如果已在后备生成器类型中定义)。Each of these keywords, and other standard F# keywords are only available in a computation expression if they have been defined in the backing builder type. 唯一的例外情况是 match!,这本身就是使用 let! 后跟结果的模式匹配的语法。The only exception to this is match!, which is itself syntactic sugar for the use of let! followed by a pattern match on the result.

生成器类型是一个对象,该对象定义控制计算表达式片段的组合方式的特殊方法;也就是说,其方法控制计算表达式的行为方式。The builder type is an object that defines special methods that govern the way the fragments of the computation expression are combined; that is, its methods control how the computation expression behaves. 描述生成器类的另一种方法是,它使您能够自定义多个F#构造(如循环和绑定)的操作。Another way to describe a builder class is to say that it enables you to customize the operation of many F# constructs, such as loops and bindings.

let!

let! 关键字将调用的结果绑定到一个名称:The let! keyword binds the result of a call to another computation expression to a name:

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

如果将对计算表达式的调用绑定到 let,则不会获得计算表达式的结果。If you bind the call to a computation expression with let, you will not get the result of the computation expression. 而是将未实现的调用的值绑定到该计算表达式。Instead, you will have bound the value of the unrealized call to that computation expression. 使用 let! 绑定到结果。Use let! to bind to the result.

let! 由生成器类型上的 Bind(x, f) 成员定义。let! is defined by the Bind(x, f) member on the builder type.

do!

do! 关键字用于调用一个计算表达式,该表达式返回类似于 unit的类型(由生成器上的 Zero 成员定义):The do! keyword is for calling a computation expression that returns a unit-like type (defined by the Zero member on the builder):

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

对于异步工作流,此类型为 Async<unit>For the async workflow, this type is Async<unit>. 对于其他计算表达式,此类型可能是 CExpType<unit>For other computation expressions, the type is likely to be CExpType<unit>.

do! 由生成器类型上的 Bind(x, f) 成员定义,其中 f 生成一个 unitdo! is defined by the Bind(x, f) member on the builder type, where f produces a unit.

yield

yield 关键字用于从计算表达式返回值,以便可以将其用作 IEnumerable<T>The yield keyword is for returning a value from the computation expression so that it can be consumed as an IEnumerable<T>:

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

for sq in squares do
    printfn "%d" sq

C#中的 yield 关键字一样,计算表达式中的每个元素都是在迭代时重新生成的。As with the yield keyword in C#, each element in the computation expression is yielded back as it is iterated.

yield 由生成器类型上的 Yield(x) 成员定义,其中 x 是要返回的项。yield is defined by the Yield(x) member on the builder type, where x is the item to yield back.

yield!

yield! 关键字用于平展计算表达式中的值的集合:The yield! keyword is for flattening a collection of values from a computation expression:

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 "%A" squaresAndCubes // Prints - 1; 4; 9; 1; 8; 27

计算时,yield! 调用的计算表达式会将其项逐个返回,从而平展结果。When evaluated, the computation expression called by yield! will have its items yielded back one-by-one, flattening the result.

yield! 由生成器类型上的 YieldFrom(x) 成员定义,其中 x 是值的集合。yield! is defined by the YieldFrom(x) member on the builder type, where x is a collection of values.

return

return 关键字包装与计算表达式对应的类型中的值。The return keyword wraps a value in the type corresponding to the computation expression. 除了使用 yield的计算表达式,它用于 "完成" 计算表达式:Aside from computation expressions using yield, it is used to "complete" a computation expression:

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

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

return 由生成器类型上的 Return(x) 成员定义,其中 x 是要包装的项。return is defined by the Return(x) member on the builder type, where x is the item to wrap.

return!

return! 关键字实现计算表达式的值,并将结果与计算表达式对应的类型进行包装:The return! keyword realizes the value of a computation expression and wraps that result in the type corresponding to the computation expression:

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

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

return! 由生成器类型上的 ReturnFrom(x) 成员定义,其中 x 是另一个计算表达式。return! is defined by the ReturnFrom(x) member on the builder type, where x is another computation expression.

match!

从 4.5 F#开始,使用match!关键字可以在其结果中内联调用另一个计算表达式和模式匹配:Starting with F# 4.5, the match! keyword allows you to inline a call to another computation expression and pattern match on its result:

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

当使用 match!调用计算表达式时,它将实现调用的结果,如 let!When calling a computation expression with match!, it will realize the result of the call like let!. 这通常在调用计算表达式(其中的结果是可选的)时使用。This is often used when calling a computation expression where the result is an optional.

内置计算表达式Built-in computation expressions

核心库定义了三个内置计算表达式:序列表达式异步工作流查询表达式。 F#The F# core library defines three built-in computation expressions: Sequence Expressions, Asynchronous Workflows, and Query Expressions.

创建新类型的计算表达式Creating a New Type of Computation Expression

您可以通过创建生成器类并在类上定义某些特殊方法,定义自己的计算表达式的特征。You can define the characteristics of your own computation expressions by creating a builder class and defining certain special methods on the class. Builder 类可以选择定义下表中列出的方法。The builder class can optionally define the methods as listed in the following table.

下表描述了可在工作流生成器类中使用的方法。The following table describes methods that can be used in a workflow builder class.

方法Method 典型签名Typical signature(s) 描述Description
Bind M<'T> * ('T -> M<'U>) -> M<'U> 为计算表达式中的 let!do! 调用。Called for let! and do! in computation expressions.
Delay (unit -> M<'T>) -> M<'T> 将计算表达式包装为函数。Wraps a computation expression as a function.
Return 'T -> M<'T> 为计算表达式中的 return 调用。Called for return in computation expressions.
ReturnFrom M<'T> -> M<'T> 为计算表达式中的 return! 调用。Called for return! in computation expressions.
Run M<'T> -> M<'T>M<'T> -> M<'T> or

M<'T> -> 'T
执行计算表达式。Executes a computation expression.
Combine M<'T> * M<'T> -> M<'T>M<'T> * M<'T> -> M<'T> or

M<unit> * M<'T> -> M<'T>
在计算表达式中调用以进行序列化。Called for sequencing in computation expressions.
For seq<'T> * ('T -> M<'U>) -> M<'U>seq<'T> * ('T -> M<'U>) -> M<'U> or

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
为计算表达式中的 for...do 表达式调用。Called for for...do expressions in computation expressions.
TryFinally M<'T> * (unit -> unit) -> M<'T> 为计算表达式中的 try...finally 表达式调用。Called for try...finally expressions in computation expressions.
TryWith M<'T> * (exn -> M<'T>) -> M<'T> 为计算表达式中的 try...with 表达式调用。Called for try...with expressions in computation expressions.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable 为计算表达式中的 use 绑定调用。Called for use bindings in computation expressions.
While (unit -> bool) * M<'T> -> M<'T> 为计算表达式中的 while...do 表达式调用。Called for while...do expressions in computation expressions.
Yield 'T -> M<'T> 为计算表达式中的 yield 表达式调用。Called for yield expressions in computation expressions.
YieldFrom M<'T> -> M<'T> 为计算表达式中的 yield! 表达式调用。Called for yield! expressions in computation expressions.
Zero unit -> M<'T> 为计算表达式中 if...then 表达式的空 else 分支调用。Called for empty else branches of if...then expressions in computation expressions.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> 指示将计算表达式作为一个引号传递到 Run 成员。Indicates that the computation expression is passed to the Run member as a quotation. 它将计算的所有实例都转换为引号。It translates all instances of a computation into a quotation.

生成器类中的许多方法都使用并返回 M<'T> 构造,这通常是一个单独定义的类型,用于确定要合并的计算种类,例如,异步工作流的 Async<'T> 和用于序列的 Seq<'T>工作.Many of the methods in a builder class use and return an M<'T> construct, which is typically a separately defined type that characterizes the kind of computations being combined, for example, Async<'T> for asynchronous workflows and Seq<'T> for sequence workflows. 这些方法的签名使它们相互组合起来并彼此嵌套,以便可以将从一个构造返回的工作流对象传递到下一个构造。The signatures of these methods enable them to be combined and nested with each other, so that the workflow object returned from one construct can be passed to the next. 编译器在分析计算表达式时,通过使用上表中的方法和计算表达式中的代码,将表达式转换为一系列嵌套函数调用。The compiler, when it parses a computation expression, converts the expression into a series of nested function calls by using the methods in the preceding table and the code in the computation expression.

嵌套表达式的格式如下:The nested expression is of the following form:

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

在上面的代码中,如果未在计算表达式生成器类中定义 RunDelay,则将忽略这些调用。In the above code, the calls to Run and Delay are omitted if they are not defined in the computation expression builder class. 计算表达式的主体(此处表示为 {| cexpr |})将按下表中所述的翻译转换为涉及生成器类方法的调用。The body of the computation expression, here denoted as {| cexpr |}, is translated into calls involving the methods of the builder class by the translations described in the following table. 根据这些转换以递归方式定义计算表达式 {| cexpr |},其中 expr 为F#表达式,cexpr为计算表达式。The computation expression {| cexpr |} is defined recursively according to these translations where expr is an F# expression and cexpr is a computation expression.

ExpressionExpression 转换Translation
{ 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 binder.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 -> reraise exn)))
{ 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 描述了表中未列出的表达式。In the previous table, other-expr describes an expression that is not otherwise listed in the table. 生成器类不需要实现所有方法并支持上表中列出的所有翻译。A builder class does not need to implement all of the methods and support all of the translations listed in the previous table. 未实现的这些构造在该类型的计算表达式中不可用。Those constructs that are not implemented are not available in computation expressions of that type. 例如,如果不想在计算表达式中支持 use 关键字,则可以在生成器类中省略 Use 的定义。For example, if you do not want to support the use keyword in your computation expressions, you can omit the definition of Use in your builder class.

下面的代码示例演示一个计算表达式,该表达式将计算封装为一系列步骤,一次只能对一个步骤进行计算。The following code example shows a computation expression that encapsulates a computation as a series of steps that can be evaluated one step at a time. OkOrException,可区分的联合类型对该表达式的错误状态进行编码(如目前所计算)。A discriminated union type, OkOrException, encodes the error state of the expression as evaluated so far. 此代码演示了几种可在计算表达式中使用的典型模式,如某些生成器方法的样本实现。This code demonstrates several typical patterns that you can use in your computation expressions, such as boilerplate implementations of some of the builder methods.

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

module Eventually =
    // The bind for the computations. Append 'func' to the
    // computation.
    let rec bind func expr =
        match expr with
        | Done value -> func value
        | NotYetDone work -> NotYetDone (fun () -> bind func (work()))

    // Return the final value wrapped in the Eventually type.
    let result value = Done value

    type OkOrException<'T> =
        | Ok of 'T
        | Exception of System.Exception

    // 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
                | Exception exn -> result (Exception 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 rest of the operations are boilerplate.
    // 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
            | Exception 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 | Exception 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.
    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

计算表达式具有表达式返回的基础类型。A computation expression has an underlying type, which the expression returns. 基础类型可能表示可以执行的计算结果或延迟计算,或者它可能提供一种方法来循环访问某些类型的集合。The underlying type may represent a computed result or a delayed computation that can be performed, or it may provide a way to iterate through some type of collection. 在上面的示例中,基础类型最终是。In the previous example, the underlying type was Eventually. 对于序列表达式,基础类型是 System.Collections.Generic.IEnumerable<T>For a sequence expression, the underlying type is System.Collections.Generic.IEnumerable<T>. 对于查询表达式,基础类型为 System.Linq.IQueryableFor a query expression, the underlying type is System.Linq.IQueryable. 对于异步工作流,基础类型是AsyncFor an asynchronous workflow, the underlying type is Async. Async 对象表示要执行的工作来计算结果。The Async object represents the work to be performed to compute the result. 例如,调用Async.RunSynchronously以执行计算并返回结果。For example, you call Async.RunSynchronously to execute a computation and return the result.

自定义操作Custom Operations

可以在计算表达式中定义自定义操作,并使用自定义操作作为计算表达式中的运算符。You can define a custom operation on a computation expression and use a custom operation as an operator in a computation expression. 例如,可以在查询表达式中包含查询运算符。For example, you can include a query operator in a query expression. 定义自定义操作时,必须在计算表达式中定义 Yield 和方法。When you define a custom operation, you must define the Yield and For methods in the computation expression. 若要定义自定义操作,请将其放在计算表达式的 builder 类中,然后应用CustomOperationAttributeTo define a custom operation, put it in a builder class for the computation expression, and then apply the CustomOperationAttribute. 此属性采用字符串作为参数,该参数是要在自定义操作中使用的名称。This attribute takes a string as an argument, which is the name to be used in a custom operation. 此名称将出现在计算表达式的左大括号开头的范围内。This name comes into scope at the start of the opening curly brace of the computation expression. 因此,不应使用与此块中的自定义操作名称相同的标识符。Therefore, you shouldn’t use identifiers that have the same name as a custom operation in this block. 例如,避免在查询表达式中使用标识符,如 alllastFor example, avoid the use of identifiers such as all or last in query expressions.

利用新的自定义操作扩展现有生成器Extending existing Builders with new Custom Operations

如果已经有一个生成器类,则可以从该生成器类的外部扩展其自定义操作。If you already have a builder class, its custom operations can be extended from outside of this builder class. 扩展必须在模块中声明。Extensions must be declared in modules. 命名空间不能包含在定义该类型的同一文件和相同的命名空间声明组中的扩展成员。Namespaces cannot contain extension members except in the same file and the same namespace declaration group where the type is defined.

下面的示例演示现有 Microsoft.FSharp.Linq.QueryBuilder 类的扩展。The following example shows the extension of the existing Microsoft.FSharp.Linq.QueryBuilder class.

type Microsoft.FSharp.Linq.QueryBuilder with

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

请参阅See also