計算運算式

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! 主要由建立器類型上的 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! 之類的呼叫結果。 一般來說,如果結果並非必要,則在呼叫計算運算式時,就會使用它。

內建計算運算式

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!,但不會合併輸入時,即呼叫。

例如, 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> 計算運算式中 if...then 運算式的 else 分支是空的時,即呼叫。
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> 表示計算運算式會以引號形式傳遞至 Run 成員。 它會將所有計算的執行個體,轉譯為引號。

建立器類別中的許多方法,都會使用以及傳回 M<'T> 建構,而這通常會是個別定義的類型,描述要結合的計算種類特性,例如 Async<'T> (非同步運算式) 和 Seq<'T> (序列工作流程)。 這些方法的特徵標記,可讓它們能彼此合併和組成巢狀結構,讓從單一建構傳回的工作流程物件,可以傳遞至下一個。

許多函式使用 Delay 的結果作為引數:RunWhileTryWithTryFinallyCombineDelayed<'T> 類型是 Delay 的傳回型別,因此是這些函式的參數。 Delayed<'T> 可以是不需要與 M<'T> 相關的任意類型;一般使用 M<'T>(unit -> M<'T>)。 預設的實作為 M<'T>。 如需更深入了解,請參閱這裡

編譯器在剖析計算運算式時,會使用上表中的方法以及計算運算式中的程式碼,將運算式轉換成一系列的巢狀函式呼叫。 巢狀運算式的格式如下:

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

在上述程式碼中,如果未在計算運算式建立器類別中,定義 RunDelay,就會略過這些呼叫。 計算運算式的主體 (此處表示為 {{ 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。 若為非同步運算式,基礎類型為 AsyncAsync 物件代表要執行以計算結果的工作。 例如,呼叫 Async.RunSynchronously 執行計算,並傳回結果。

自訂作業

您可以對計算運算式定義自訂運算,並在計算運算式中使用自訂運算當作運算子。 例如,可以在查詢運算式中包含查詢運算子。 當您定義自訂運算時,必須在計算運算式中定義 Yield 和 For 方法。 若要定義自訂運算,請將其置於計算運算式的建立器類別中,然後套用 CustomOperationAttribute。 這個屬性會採用字串作為引數,此為自訂運算中將使用的名稱。 這個名稱的範圍開頭,是計算運算式的左大括號。 因此,不應使用和此區塊中自訂運算相同名稱的識別碼。 例如,請避免在查詢運算式中使用像是 lastall 識別碼。

使用新的自訂運算擴充現有建立器

如果已經有建立器類別,可以從這個建立器類別,擴充其自訂運算。 延伸模組必須宣告在模組中。 命名空間不可包含延伸模組成員,除非在相同檔案中,定義該類型的相同命名空間宣告群組也除外。

下列範例顯示現有 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 (英文) 中說明可繼續的程式碼,並可用於 工作運算式

使用 內嵌函式 (包含 InlineIfLambda 屬性),也可以將同步 (也就是不會暫停執行) 的 F# 計算運算式,編譯為高效的狀態機器。 F# RFC FS-1098 (英文) 中提供範例。

F# 編譯器可為清單運算式、陣列運算式和序列運算式,提供特殊處理,以確保能產生高效的程式碼。

另請參閱