F# 中的计算表达式提供了一种方便的语法,用于编写可以使用控制流构造和绑定对计算进行排序和组合的计算。 根据计算表达式的类型,可以将其视为表达 monad、monoid、monad 转换器和适用性函数的一种方式。 但是,与其他语言(如 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! 相结合的计算不能依赖于以前绑定的值在同一 let!/and! 链中的结果。 这种权衡可实现性能优势。
and! 主要由 MergeSources(x1, x2) 生成器类型上的成员定义。
(可选)可以定义MergeSourcesN(x1, x2 ..., xN)以减少元组化节点的数量,并且可以定义BindN(x1, x2 ..., xN, f)或BindNReturn(x1, x2, ..., xN, f)以便在不使用元组化节点的情况下高效绑定计算表达式的结果。
有关适用性计算表达式的详细信息,请参阅 F# 5 和 F# RFC FS-1063中的适用性计算表达式。
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!。 当调用计算表达式(其中结果为 可选)时,通常使用此表达式。
内置计算表达式
F# 核心库定义了四个内置计算表达式: 序列表达式、 异步表达式、 任务表达式和 查询表达式。
创建新类型的计算表达式
可以通过创建生成器类并在类上定义某些特殊方法来定义自己的计算表达式的特征。 生成器类可以选择性地定义下表中列出的方法。
下表描述了可在工作流生成器类中使用的方法。
| 方法 | 典型签名(s) | 说明 |
|---|---|---|
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!。例如,. Bind3Bind4 |
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 ,无需合并输入。例如,. Bind3ReturnBind4Return |
MergeSources |
(M<'T1> * M<'T2>) -> M<'T1 * 'T2> |
在 and! 计算表达式中调用。 |
MergeSourcesN |
(M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> |
在计算表达式中调用and!,但通过减少元组节点的数量来提高效率。例如,. MergeSources3MergeSources4 |
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、、WhileTryWith、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 表示计算表达式。
| 表达式 | 翻译 |
|---|---|
{{ 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>]
member _.any (source: QuerySource<'T, 'Q>, predicate) =
System.Linq.Enumerable.Any (source.Source, Func<_,_>(predicate))
[<CustomOperation("singleSafe")>] // you can specify your own operation name in the constructor
member _.singleOrDefault (source: QuerySource<'T, 'Q>, predicate) =
System.Linq.Enumerable.SingleOrDefault (source.Source, Func<_,_>(predicate))
可以重载自定义操作。 有关详细信息,请参阅 F# RFC FS-1056 - 允许在计算表达式中重载自定义关键字。
高效编译计算表达式
通过精确使用一种名为可恢复代码的低级功能,可以将挂起执行的 F# 计算表达式编译为高效的状态机。 可恢复代码记录在 F# RFC FS-1087 中,并用于 任务表达式。
同步的 F# 计算表达式(即它们不会挂起执行)也可以通过使用 内联函数 (包括 InlineIfLambda 属性)编译为高效的状态机。
F# RFC FS-1098 中提供了示例。
F# 编译器为列表表达式、数组表达式和序列表达式提供特殊处理,以确保生成高性能代码。