F# の計算式は、制御フローのコンストラクトとバインドを使用してシーケンス化および結合できる計算を記述するための便利な構文を提供します。 計算式の種類によっては、モナド、モノイド、モナドトランスフォーマー、適用可能なファンクタを表現する方法と考えることができます。 ただし、他の言語 (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! の使用のための構文糖衣構造であり、結果に対してパターンマッチを行うものです。
ビルダー型は、コンピュテーション式のフラグメントの結合方法を制御する特別なメソッドを定義するオブジェクトです。つまり、そのメソッドはコンピュテーション式の動作を制御します。 ビルダー クラスを記述するもう 1 つの方法は、ループやバインドなど、多くの 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) を定義して tupling ノードの数を減らすことができます。また、計算式の結果を効率的にバインドするために、tupling ノードを使用せずに 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! によって呼び出された計算式の項目が 1 つずつ返され、結果がフラット化されます。
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# コア ライブラリでは、シーケンス式、非同期式、タスク式、クエリ式の 4 つの組み込み計算式が定義されています。
新しい種類のコンピュテーション式の作成
ビルダー クラスを作成し、クラスに特定の特殊なメソッドを定義することで、独自の計算式の特性を定義できます。 ビルダー クラスは、必要に応じて、次の表に示すようにメソッドを定義できます。
次の表では、ワークフロー ビルダー クラスで使用できるメソッドについて説明します。
| 方式 | 一般的な署名 | 説明 |
|---|---|---|
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! に対して呼び出されます。 |
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 のために呼び出されます。たとえば、 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! 式に対して呼び出されます。 |
YieldFromFinal |
M<'T> -> M<'T> |
存在する場合は、末尾呼び出し位置にあるときに yield! を呼び出し、末尾呼び出し位置でフォールバックとして do! する場合に呼び出されます。 ReturnFromFinal |
Zero |
unit -> M<'T> |
計算式のelse式の空のif...then分岐に対して呼び出されます。 |
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 の定義を省略できます。
次のコード例は、一度に 1 ステップずつ評価できる一連のステップとして計算をカプセル化する計算式を示しています。 判別共用体の型 ( 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# コンパイラによって特別な処理が行われます。
こちらも参照ください
.NET