计算表达式
F# 中的计算表达式提供一种用于编写计算的便捷语法,可以通过使用控制流构造和绑定对这些计算进行排序和组合。 根据计算表达式的类型,可以将其视为表示 monad、monoids、monad 转换器和 applicative functor 的一种方式。 但是,与其他语言(例如 Haskell 中的 do-notation)不同,它们不依赖于单一抽象,也不依赖宏或其他形式的元编程来实现上下文相关的便捷语法。
概述
计算可以采用多种形式。 最常见的计算形式是单线程执行,这种形式易于理解和修改。 但是,并非所有形式的计算都像单线程执行一样简单。 示例包括:
- 非确定性计算
- 异步计算
- 有效计算
- 生成计算
一般来说,上下文相关计算必须在应用程序的特定部分执行。 编写上下文相关代码可能会很难,因为很容易将计算“泄漏”到给定上下文之外,而没有抽象来阻止你这样做。 自行编写这些抽象通常很难,这就是为什么 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 { and! ... }
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)
成员定义。
and!
通过 and!
关键字,可以高性能方式绑定多个计算表达式调用的结果。
let doThingsAsync url =
async {
let! data = getDataAsync url
and! moreData = getMoreDataAsync anotherUrl
and! evenMoreData = getEvenMoreDataAsync someUrl
...
}
使用一系列 let! ... let! ...
会强制重新执行成本较高的绑定,因此在绑定大量计算表达式的结果时应该使用 let! ... and! ...
。
and!
主要由生成器类型上的 MergeSources(x1, x2)
成员定义。
(可选)可以定义 MergeSourcesN(x1, x2 ..., xN)
以减少元组节点的数量,并且可以定义 BindN(x1, x2 ..., xN, f)
或 BindNReturn(x1, x2, ..., xN, 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
是要包装的项。 对于 let! ... return
的使用,BindReturn(x, f)
可用于提高性能。
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!
那样实现调用的结果。 这通常在调用结果为 optional 的计算表达式时使用。
内置计算表达式
F# 核心库定义了四个内置计算表达式:序列表达式、异步表达式、任务表达式和查询表达式。
创建新型计算表达式
你可以通过创建生成器类并在该类上定义某些特殊方法,来定义自己的计算表达式的特征。 生成器类可以选择定义下表中列出的方法。
下表描述了可在工作流生成器类中使用的方法。
方法 | 典型签名 | 说明 |
---|---|---|
Bind |
M<'T> * ('T -> M<'U>) -> M<'U> |
在计算表达式中为 let! 和 do! 调用。 |
BindN |
(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> |
在不合并输入的情况下在计算表达式中为高效的 let! 和 and! 调用。例如 Bind4 和 Bind3 。 |
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! 调用。 |
BindReturn |
(M<'T1> * ('T1 -> 'T2)) -> M<'T2> |
在计算表达式中为高效的 let! ... return 调用。 |
BindNReturn |
(M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> |
在不合并输入的情况下在计算表达式中为高效的 let! ... and! ... return 调用。例如 Bind4Return 和 Bind3Return 。 |
MergeSources |
(M<'T1> * M<'T2>) -> M<'T1 * 'T2> |
在计算表达式中为 and! 调用。 |
MergeSourcesN |
(M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> |
在计算表达式中为 and! 调用,但通过减少元组节点的数量来提高效率。例如 MergeSources4 和 MergeSources3 。 |
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> |
在计算表达式中为 if...then 表达式的空 else 分支调用。 |
Quote |
Quotations.Expr<'T> -> Quotations.Expr<'T> |
指示计算表达式作为引用传递给 Run 成员。 它会将计算的所有实例都转换为引用。 |
生成器类中的许多方法使用并返回 M<'T>
构造,该构造通常是一个单独定义的类型,用于表征要组合的计算类型,例如,Async<'T>
用于异步表达式,Seq<'T>
用于序列工作流。 这些方法的签名使它们能够相互组合和嵌套,以便将从一个构造返回的工作流对象传递给下一个构造。
许多函数使用 Delay
的结果作为参数:Run
、While
、TryWith
、TryFinally
和 Combine
。 Delayed<'T>
类型是 Delay
的返回类型,因此是这些函数的参数。 Delayed<'T>
可以是不需要与 M<'T>
相关的任意类型;通常使用 M<'T>
或 (unit -> M<'T>)
。 默认实现是 M<'T>
。 请参阅此处详加了解。
编译器在分析计算表达式时,通过使用上表中的方法和计算表达式中的代码,将表达式转换为一系列嵌套的函数调用。 嵌套表达式采用以下形式:
builder.Run(builder.Delay(fun () -> {{ cexpr }}))
在上面的代码中,如果 Run
和 Delay
未在计算表达式生成器类中定义,则会省略对它们的调用。 计算表达式的主体(此处表示为 {{ 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 enumerable-expr do cexpr }} |
builder.For(enumerable-expr, (fun pattern -> {{ cexpr }})) |
{{ for identifier = expr1 to expr2 do cexpr }} |
builder.For([expr1..expr2], (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 -> Error 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。 对于异步表达式,基础类型是 Async
。 Async
对象表示计算结果所要执行的工作。 例如,调用 Async.RunSynchronously
来执行计算并返回结果。
自定义操作
你可以对计算表达式定义自定义操作,并将自定义操作用作计算表达式中的运算符。 例如,可以在查询表达式中包含查询运算符。 定义自定义操作时,必须在计算表达式中定义 Yield 和 For 方法。 若要定义自定义操作,请将其放入计算表达式的生成器类中,然后应用 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 - Allow overloads of custom keywords in computation expressions(F# RFC FS-1056 - 允许在计算表达式中重载自定义关键字)。
高效编译计算表达式
通过谨慎使用称为可恢复代码的低级功能,可以将暂停执行的 F# 计算表达式编译为高效的状态机。 可恢复代码记录在 F# RFC FS-1087 中并用于任务表达式。
通过使用包含 InlineIfLambda
属性的内联函数,同步的 F# 计算表达式(即,它们不会暂停执行)也可以编译为高效的状态机。 F# RFC FS-1098 中提供了示例。
F# 编译器对列表表达式、数组表达式和序列表达式进行了特殊处理,以确保生成高性能代码。