コンピュテーション式

F# のコンピュテーション式には、制御フローのコンストラクトとバインドを使用してシーケンス化および結合できる計算を記述するための便利な構文が用意されています。 コンピュテーション式は、その種類に応じて、モナド、モノイド、モナド変換子、およびアプリカティブ ファンクターを表現する方法と考えることができます。 ただし、他の言語 (Haskell の do-notation など) とは異なり、それらは 1 つの抽象化に関連付けられておらず、便利な状況依存の構文を実現するためにマクロやその他の形式のメタプログラミングに依存しません。

概要

計算にはさまざまな形式があります。 計算の最も一般的な形式は、理解と変更が簡単なシングルスレッド実行です。 ただし、すべての形式の計算がシングルスレッド実行ほど単純であるとは限りません。 次に例をいくつか示します。

  • 非決定的な計算
  • 非同期計算
  • 有効計算
  • 生成的計算

より一般には、アプリケーションの特定の部分で実行する必要がある "状況依存" の計算があります。 状況依存のコードを記述することは難しい場合があります。これは、抽象化によって回避しないと特定のコンテキストの外部に計算が "リーク" しやすいからです。 多くの場合、これらの抽象化は自分で記述することが困難です。そのため、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! によって呼び出されるコンピュテーション式の項目が 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! に対して呼び出されます。

例: 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>or

(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> など、結合される計算の種類を特徴付ける個別に定義された型です。 これらのメソッドのシグネチャを使用すると、1 つのコンストラクトから返されるワークフロー オブジェクトを次のものに渡すことができるように、それらを組み合わせて入れ子にすることができます。

多くの関数 (RunWhileTryWithTryFinallyCombine) では、Delay の結果が引数として使用されます。 型 Delayed<'T> は、Delay の戻り値の型であるため、これらの関数のパラメーターです。 Delayed<'T> には、M<'T> に関連するとは限らない任意の型を指定でき、通常は M<'T> または (unit -> M<'T>) が使用されます。 既定の実装は M<'T> です。 詳細については、こちらを参照してください。

コンパイラが評価式を解析する際には、前の表にあるメソッドと評価式内のコードを使用することで、その式が一連の入れ子になった関数呼び出しに翻訳されます。 入れ子になった式の形式は次のとおりです。

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

上記のコードで、RunDelay は、コンピュテーション式ビルダー クラス内で定義されていない場合に省略されます。 ここで、{{ cexpr }} として示されている評価式の本体は、ビルダー クラスのメソッドに対するさらなる呼び出しに翻訳されます。 このプロセスは、次の表での翻訳に従って再帰的に定義されています。 二重ブラケット {{ ... }} 内のコードは翻訳されるために保持され、 expr は F# 式を表し、 cexpr は評価式を表しています。

Expression 翻訳
{{ 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 を適用します。 この属性は、文字列を引数として受け取ります。これはカスタム操作で使用される名前です。 この名前は、コンピュテーション式の左中かっこの先頭にあるスコープに含まれます。 したがって、このブロック内ではカスタム操作と同じ名前を持つ識別子は使用しないでください。 たとえば、クエリ式で alllast などの識別子を使用しないようにします。

新しいカスタム操作を使用した既存のビルダーの拡張

既にビルダー クラスがある場合は、そのカスタム操作をこのビルダー クラスの外部から拡張できます。 拡張はモジュール内で宣言する必要があります。 名前空間には、同じファイルと、型が定義されている同じ名前空間宣言グループを除き、拡張メンバーを含めることはできません。

次の例は、既存の 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」で説明されており、「タスク式」で使用されています。

同期的な (つまり、実行を中断しない) F# コンピュテーション式は、InlineIfLambda 属性を含むインライン関数を使用することで効率的なステート マシンにコンパイルすることもできます。 例が、F# RFC FS-1098 に示されています。

リスト式、配列式、シーケンス式には、ハイ パフォーマンス コードの生成を確保するために、F# コンパイラによる特別な処理を行います。

関連項目