計算運算式 (F#)
F# 中的計算運算式提供便利的語法,撰寫可以使用控制流程建構和繫結進行排序和合併的計算。它們可以用來提供 Monad 的便利語法,這是一種函式程式設計功能,可用來管理函式程式中的資料、控制項和副作用。
內建工作流程
序列的運算式是一個範例的計算運算式,非同步工作流程和查詢運算式。如需詳細資訊,請參閱序列, 非同步工作流程,以及 查詢運算式。
某些功能是序列運算式和非同步工作流程通用的,並說明計算運算式的基本語法:
builder-name { expression }
前一個語法指定所指定的運算式是計算運算式,而計算運算式的型別是透過 builder-name 所指定。計算運算式可以是內建工作流程 (例如 seq 或 async),也可以是您所定義的任何項目。builder-name 為特殊型別 (稱為「產生器型別」(Builder Type)) 之執行個體的識別項。產生器型別為定義特殊方法的類別型別,而這些方法可管控計算運算式片段的合併方式,即控制運算式執行方式的程式碼。另一種描述產生器類別的方法說明它可讓您自訂多個 F# 建構 (例如迴圈和繫結) 的作業。
在計算運算式中,有兩個形式可用於某些通用語言建構。您可以叫用變數建構,方式是使用 !(驚嘆號) 後置字元,例如 let!、do! 等等,即可叫用各種建構。這些特殊形式會讓產生器類別中定義的某些函式取代這些作業的一般內建行為。這些形式類似序列運算式中使用之 yield! 形式的 yield 關鍵字。如需詳細資訊,請參閱序列。
建立計算運算式的新型別
建立產生器類別以及在類別上定義某個特殊方法,就可以定義專屬計算運算式的特性。產生器類別可以選擇性地定義下表所列的方法。
下表描述可在工作流程產生器類別中使用的方法。
方法 |
一般簽章 |
描述 |
Bind |
M<'T> * ('T -> M<'U>) -> M<'U> |
針對計算運算式中的 let! 和 do! 呼叫。 |
Delay |
(unit -> M<'T>) -> M<'T> |
將計算運算式包裝為函式。 |
Return |
'T -> M<'T> |
針對計算運算式中的 return 呼叫。 |
ReturnFrom |
M<'T> -> M<'T> |
針對計算運算式中的 return! 呼叫。 |
Run |
M<'T> -> M<'T> 或 M<'T> -> 'T |
執行計算運算式。 |
Combine |
M<'T> * M<'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 |
M<'T> * (unit -> unit) -> M<'T> |
針對計算運算式中的 try...finally 運算式呼叫。 |
TryWith |
M<'T> * (exn -> M<'T>) -> M<'T> |
針對計算運算式中的 try...with 運算式呼叫。 |
Using |
'T * ('T -> M<'U>) -> M<'U> when 'U :> IDisposable |
針對計算運算式中的 use 繫結呼叫。 |
While |
(unit -> bool) * M<'T> -> M<'T> |
針對計算運算式中的 while...do 運算式呼叫。 |
Yield |
'T -> M<'T> |
針對計算運算式中的 yield 運算式呼叫。 |
YieldFrom |
M<'T> -> M<'T> |
針對計算運算式中的 yield! 運算式呼叫。 |
Zero |
unit -> M<'T> |
針對計算運算式中 if...then 運算式的 else 空分支呼叫。 |
許多產生器類別中方法的使用,將M<'T>建構,通常是個別定義的型別特性化的一種運算結合,比方說, Async<'T>為非同步工作流程和Seq<'T>用於順序的工作流程。這些方法的簽章可讓它們彼此合併和進行巢狀處理,因此可以將某個建構傳回的工作流程物件傳遞給下一個建構。編譯器在剖析計算運/算式時,會將運算式轉換為一連串的巢狀函式呼叫,方法是使用前一張表格中的方法和計算運算式中的程式碼。
巢狀運算式會採用下列格式:
builder.Run(builder.Delay(fun () -> {| cexpr |}))
在上述的程式碼中,如果 Run 和 Delay 的呼叫不是在計算運算式產生器類別中定義,則會加以省略。計算運算式的主體 (此處以 {| cexpr |} 描述) 會藉由下表中所述的轉譯方式,轉譯成包含產生器類別之方法的呼叫。計算運算式 {| 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(expr1, (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 描述未列於表中的運算式。產生器不需要實作所有方法及支援上表中列出的所有轉譯。這些未實作的建構無法在該型別的計算運算式中使用。例如,如果您不想要在計算運算式中支援 use 關鍵字,可以在產生器類別中省略 Use 的定義。
下列程式碼範例顯示的計算運算式會封裝計算,如一系列的步驟,可以一次評估一次。A discriminated 聯集的型別, OkOrException,計算到目前為止,將編碼這個運算式的錯誤狀態。這段程式碼會示範數個常見的模式,您可以在計算運算式,例如未定案的部分產生器方法的實作方式中使用。
// 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 -> NotYetDone (fun () -> 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 "NotYetDone <closure>"
comp |> step |> step |> step |> step |> step |> step
// prints "x = 1"
// prints "x = 2"
// returns "Done 7"
comp |> step |> step |> step |> step |> step |> step |> step |> step
計算運算式有基礎型別,它會傳回運算式。基礎型別代表的計算的結果或延遲的計算,只可以執行,或者它可能會提供一個方法來逐一查看集合的某些型別。在前一個範例中,已基礎型別Eventually。序列的運算式中,為基礎型別是IEnumerable<T>。查詢運算式的基礎型別是IQueryable<T>。非同步工作流程的基礎型別是非同步。Async物件所表示的工作,以執行來計算結果。例如,呼叫 Async.RunSynchronously 來執行計算,並傳回結果。
自訂作業
可以定義自訂的作業,在計算運算式,並在計算運算式中運算子使用自訂作業。比方說,您可以在查詢運算式中包含的查詢運算子。當您定義自訂的作業時,您必須定義收益率,並在計算運算式的方法。若要定義自訂的作業,請將它放在計算運算式建立幫手] 類別中,然後再套用 CustomOperationAttribute。這個屬性會取得一個字串,做為引數,也就是要用於自訂作業的名稱。這個名稱會收集到的左大括弧,計算運算式的開頭的範圍。因此,您不應該使用具有自訂的作業相同的名稱,在這個區塊中的識別項。比方說,例如避免識別項的使用all或last在查詢運算式。