共用方式為


計算表達式

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 是值的集合。

不同於 yieldyield! 必須明確指定 。 其行為並不隱含於計算表達式中。

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!
ReturnFromFinal M<'T> -> M<'T> 如果存在,則在尾部呼叫位置時被呼叫return!do!
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! 表達式。
YieldFromFinal M<'T> -> M<'T> 如果存在,則在處於尾調用位置時調用,在尾do!調用位置時調用yield!,作為ReturnFromFinal
Zero unit -> M<'T> 呼叫計算表達式中表達式的else空白if...then分支。
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> 表示計算表達式會以引號形式傳遞至 Run 成員。 它會將所有計算實例轉譯成引號。

建置器類別中的許多方法都會使用 並傳回 M<'T> 建構,這通常是個別定義的型別,描述要結合的計算類型,例如 Async<'T> ,異步表達式和 Seq<'T> 序列工作流程。 這些方法的簽章允許它們被彼此結合和嵌套,使從一個結構返回的工作流程對象能夠傳遞至下一個結構。

許多函式會使用 的結果Delay做為自變數:Run、、WhileTryWithTryFinally、 和 CombineDelayed<'T> 的回傳型別是 Delay,因此 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。 這個屬性會採用字串做為自變數,這是自定義作業中要使用的名稱。 此名稱在運算式的起始大括號處進入範疇。 因此,您不應該使用此區塊中具有與自定義作業相同名稱的標識碼。 例如,請避免在查詢表示式中使用 例如 或 alllast標識符。

使用新的自定義作業擴充現有的產生器

如果您已經有建置器類別,則可以從這個產生器類別之外擴充其自定義作業。 延伸模組必須在模組中宣告。 命名空間不能包含擴充成員,但在定義類型的相同檔案和相同命名空間宣告群組中除外。

下列範例顯示現有 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# 計算運算式(也就是不會暫停執行)可以透過使用內聯函數,包括 屬性,編譯成有效率的狀態機器。 F# RFC FS-1098 提供範例。

F# 編譯程式會提供列表表示式、陣列表示式和序列表示式的特殊處理,以確保產生高效能程式代碼。

另請參閱