計算運算式
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! ,但不會合併輸入時,即呼叫。例如, Bind3 、Bind4 。 |
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 ,但不會合併輸入時,即呼叫。例如, Bind3Return 、Bind4Return 。 |
MergeSources |
(M<'T1> * M<'T2>) -> M<'T1 * 'T2> |
計算運算式中有 and! 時,即呼叫。 |
MergeSourcesN |
(M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> |
計算運算式中有 and! 時即呼叫,但可藉由減少元組節點數目,來提升效率。例如, MergeSources3 、MergeSources4 。 |
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
代表計算運算式。
運算式 | 翻譯 |
---|---|
{{ 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
。 這個屬性會採用字串作為引數,此為自訂運算中將使用的名稱。 這個名稱的範圍開頭,是計算運算式的左大括號。 因此,不應使用和此區塊中自訂運算相同名稱的識別碼。 例如,請避免在查詢運算式中使用像是 last
或 all
識別碼。
使用新的自訂運算擴充現有建立器
如果已經有建立器類別,可以從這個建立器類別,擴充其自訂運算。 延伸模組必須宣告在模組中。 命名空間不可包含延伸模組成員,除非在相同檔案中,定義該類型的相同命名空間宣告群組也除外。
下列範例顯示現有 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# 編譯器可為清單運算式、陣列運算式和序列運算式,提供特殊處理,以確保能產生高效的程式碼。