Berechnungsausdrücke

Berechnungsausdrücke in F# bieten eine bequeme Syntax für das Schreiben von Berechnungen, die mithilfe von Ablaufsteuerungskonstrukten und Bindungen sequenziert und kombiniert werden können. Je nach Art des Berechnungsausdrucks können sie als Möglichkeit betrachtet werden, Monade, Monoide, Monadtransformatoren und applikative Funktoren auszudrücken. Im Gegensatz zu anderen Sprachen (z. B. der do-Notation in Haskell) sind sie jedoch nicht an eine einzelne Abstraktion gebunden und benötigen keine Makros oder andere Formen der Metaprogrammierung, um eine bequeme und kontextabhängige Syntax bereitzustellen.

Übersicht

Berechnungen gibt es in unterschiedlichen Formen. Die häufigste Form der Berechnung ist die Einzelthreadausführung, die leicht zu verstehen und zu ändern ist. Allerdings sind nicht alle Berechnungsformen so einfach wie die Einzelthreadausführung. Beispiele hierfür sind:

  • Nicht deterministische Berechnungen
  • Asynchrone Berechnungen
  • Effektvolle Berechnungen
  • Generative Berechnungen

Allgemeiner sind kontextabhängige Berechnungen, die Sie in bestimmten Teilen einer Anwendung ausführen müssen. Das Schreiben von kontextabhängigem Code kann schwierig sein, da Berechnungen leicht aus einem bestimmten Kontext nach außen gelangen können, ohne durch Abstraktionen daran gehindert zu werden. Es ist oft schwierig, diese Abstraktionen selbst zu schreiben; daher bietet F# eine generalisierte Möglichkeit dafür: die so genannten Berechnungsausdrücke.

Berechnungsausdrücke bieten ein einheitliches Syntax- und Abstraktionsmodell für die Programmierung kontextabhängiger Berechnungen.

Jeder Berechnungsausdruck wird durch einen Buildertyp unterstützt. Der Buildertyp definiert die Vorgänge, die für den Berechnungsausdruck verfügbar sind. Informationen zum Erstellen eines benutzerdefinierten Berechnungsausdrucks finden Sie unter Erstellen einer neuen Art von Berechnungsausdruck.

Syntaxübersicht

Alle Berechnungsausdrücke weisen die folgende Form auf:

builder-expr { cexper }

In dieser Form ist builder-expr der Name eines Buildertyps, der den Berechnungsausdruck definiert, und cexper ist der Ausdruckstext des Berechnungsausdrucks. Beispielsweise kann async-Berechnungsausdruckscode wie folgt aussehen:

let fetchAndDownload url =
    async {
        let! data = downloadData url

        let processedData = processData data

        return processedData
    }

Es gibt eine zusätzliche spezielle Syntax, die in einem Berechnungsausdruck verfügbar ist, wie im vorherigen Beispiel gezeigt. Bei Berechnungsausdrücken sind folgende Ausdrucksformen möglich:

expr { let! ... }
expr { and! ... }
expr { do! ... }
expr { yield ... }
expr { yield! ... }
expr { return ... }
expr { return! ... }
expr { match! ... }

Jedes dieser Schlüsselwörter sowie andere standardmäßige F#-Schlüsselwörter sind nur in einem Berechnungsausdruck verfügbar, wenn sie im unterstützenden Buildertyp definiert wurden. Die einzige Ausnahme ist das Schlüsselwort match!, das selbst syntaktischer Zucker für die Verwendung von let! gefolgt von einem Musterabgleich im Ergebnis ist.

Der Buildertyp ist ein Objekt, das spezielle Methoden definiert, die steuern, wie die Fragmente des Berechnungsausdrucks kombiniert werden; d. h. diese Methoden steuern, wie sich der Berechnungsausdruck verhält. Builderklassen lassen sich auch dadurch beschreiben, dass Sie die Verarbeitung vieler F#-Konstrukte wie z. B. Schleifen und Bindungen anpassen können.

let!

Das Schlüsselwort let! bindet das Ergebnis eines Aufrufs eines anderen Berechnungsausdrucks an einen Namen:

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        ...
    }

Wenn Sie den Aufruf mit let an einen Berechnungsausdruck binden, erhalten Sie das Ergebnis des Berechnungsausdrucks nicht. Stattdessen haben Sie den Wert des nicht ausgeführten Aufrufs dieses Berechnungsausdrucks gebunden. Verwenden Sie let!, um eine Bindung an das Ergebnis zu erstellen.

let! wird vom Bind(x, f)-Member im Buildertyp definiert.

and!

Mit dem Schlüsselwort and! können Sie die Ergebnisse mehrerer Aufrufe von Berechnungsausdrücken auf performante Weise binden.

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        and! moreData = getMoreDataAsync anotherUrl
        and! evenMoreData = getEvenMoreDataAsync someUrl
        ...
    }

Die Verwendung einer Reihe von let! ... let! ... erzwingt die erneute Ausführung teurer Bindungen, sodass zum Binden zahlreicher Berechnungsausdrücke let! ... and! ... verwendet werden sollte.

and! wird in erster Linie durch den MergeSources(x1, x2)-Member im Buildertyp definiert.

Optional kann MergeSourcesN(x1, x2 ..., xN) definiert werden, um die Anzahl der Tupelknoten zu verringern, und BindN(x1, x2 ..., xN, f) oder BindNReturn(x1, x2, ..., xN, f) können definiert werden, um Ergebnisse von Berechnungsausdrücken effizient ohne Tupelknoten zu binden.

do!

Das Schlüsselwort do! dient zum Aufrufen eines Berechnungsausdrucks, der einen Typ ähnlich unit zurückgibt (definiert durch den Zero-Member im Builder):

let doThingsAsync data url =
    async {
        do! submitData data url
        ...
    }

Für den asynchronen Workflow lautet dieser Typ Async<unit>. Bei anderen Berechnungsausdrücken ist der Typ wahrscheinlich CExpType<unit>.

do! wird durch den Bind(x, f)-Member im Buildertyp definiert, wobei f ein unit-Element erzeugt.

yield

Das Schlüsselwort yield dient zum Zurückgeben eines Werts aus dem Berechnungsausdruck, sodass dieser als IEnumerable<T> verwendet werden kann:

let squares =
    seq {
        for i in 1..10 do
            yield i * i
    }

for sq in squares do
    printfn $"%d{sq}"

In den meisten Fällen kann es von aufrufenden Funktionen weggelassen werden. Die am häufigsten verwendete Methode zum Weglassen von yield ist der ->-Operator:

let squares =
    seq {
        for i in 1..10 -> i * i
    }

for sq in squares do
    printfn $"%d{sq}"

Bei komplexeren Ausdrücken, die möglicherweise viele verschiedene Werte liefern und möglicherweise Bedingungen unterliegen, kann ein einfaches Weglassen des Schlüsselworts ausreichen:

let weekdays includeWeekend =
    seq {
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        if includeWeekend then
            "Saturday"
            "Sunday"
    }

Wie bei dem Schlüsselwort „yield“ in C# wird jedes Element im Berechnungsausdruck beim Durchlaufen sofort zurückgegeben.

yield wird durch den Yield(x)-Member im Buildertyp definiert, wobei x das Element ist, das zurückgegeben werden soll.

yield!

Das Schlüsselwort yield! dient zum Vereinfachen einer Auflistung von Werten aus einem Berechnungsausdruck:

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

Bei der Auswertung erhält der von yield! aufgerufene Berechnungsausdruck seine Elemente einzeln zurück, wodurch das Ergebnis vereinfacht wird.

yield! wird durch den YieldFrom(x)-Member im Buildertyp definiert, wobei x eine Auflistung von Werten ist.

Im Gegensatz zu yield muss yield! explizit angegeben werden. Das Verhalten ist in Berechnungsausdrücken nicht implizit.

return

Das Schlüsselwort return umschließt einen Wert in dem Typ, der dem Berechnungsausdruck entspricht. Abgesehen von Berechnungsausdrücken mit yield wird es verwendet, um einen Berechnungsausdruck „abzuschließen“:

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 wird durch den Return(x)-Member im Buildertyp definiert, wobei x das Element ist, das umschlossen werden soll. Bei let! ... return kann BindReturn(x, f) zur Verbesserung der Leistung verwendet werden.

return!

Das Schlüsselwort return! erkennt den Wert eines Berechnungsausdrucks und umschließt dieses Ergebnis in dem Typ, der dem Berechnungsausdruck entspricht:

let req = // 'req' is of type 'Async<data>'
    async {
        return! fetch url
    }

// 'result' is of type 'data'
let result = Async.RunSynchronously req

return! wird durch den ReturnFrom(x)-Member im Buildertyp definiert, wobei x ein weiterer Berechnungsausdruck ist.

match!

Mit dem Schlüsselwort match! können Sie einen Aufruf eines anderen Berechnungsausdrucks und den Musterabgleich für dessen Ergebnis inline einfügen:

let doThingsAsync url =
    async {
        match! callService url with
        | Some data -> ...
        | None -> ...
    }

Beim Aufrufen eines Berechnungsausdrucks mit match! wird das Ergebnis eines Aufrufs wie let! erkannt. Dies wird häufig beim Aufrufen eines Berechnungsausdrucks verwendet, bei dem das Ergebnis optional ist.

Integrierte Berechnungsausdrücke

Die F#-Kernbibliothek definiert vier integrierte Berechnungsausdrücke: Sequenzausdrücke, Asynchrone Ausdrücke, Aufgabenausdrücke und Abfrageausdrücke.

Erstellen einer neuen Art von Berechnungsausdruck

Sie können die Merkmale Ihrer eigenen Berechnungsausdrücke definieren, indem Sie eine Builderklasse erstellen und bestimmte spezielle Methoden für die Klasse definieren. Die Builderklasse kann optional die in der folgenden Tabelle aufgeführten Methoden definieren.

In der folgenden Tabelle werden Methoden beschrieben, die in einer Workflowbuilderklasse verwendet werden können.

Methode Typische Signatur(en) Beschreibung
Bind M<'T> * ('T -> M<'U>) -> M<'U> Wird für let! und do! in Berechnungsausdrücken aufgerufen.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Wird für die effiziente Verwendung von let! und and! in Berechnungsausdrücken ohne Zusammenführen von Eingaben aufgerufen.

Beispiele: Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Umschließt einen Berechnungsausdruck als Funktion. Delayed<'T> kann ein beliebiger Typ sein, häufig werden M<'T> oder unit -> M<'T> verwendet. Die Standardimplementierung gibt einen M<'T>-Typ zurück.
Return 'T -> M<'T> Wird für return in Berechnungsausdrücken aufgerufen.
ReturnFrom M<'T> -> M<'T> Wird für return! in Berechnungsausdrücken aufgerufen.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Wird für die effiziente Verwendung von let! ... return in Berechnungsausdrücken aufgerufen.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Wird für die effiziente Verwendung von let! ... and! ... return in Berechnungsausdrücken ohne Zusammenführen von Eingaben aufgerufen.

Beispiele: Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> Wird für and! in Berechnungsausdrücken aufgerufen.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Wird für and! in Berechnungsausdrücken aufgerufen, verbessert jedoch die Effizienz durch Reduzieren der Anzahl von Tupelknoten.

Beispiele: MergeSources3, MergeSources4.
Run Delayed<'T> -> M<'T> oder

M<'T> -> 'T
Führt einen Berechnungsausdruck aus.
Combine M<'T> * Delayed<'T> -> M<'T> oder

M<unit> * M<'T> -> M<'T>
Wird für die Sequenzierung in Berechnungsausdrücken aufgerufen.
For seq<'T> * ('T -> M<'U>) -> M<'U> oder

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Wird für for...do-Ausdrücke in Berechnungsausdrücken aufgerufen.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Wird für try...finally-Ausdrücke in Berechnungsausdrücken aufgerufen.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Wird für try...with-Ausdrücke in Berechnungsausdrücken aufgerufen.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Wird für use-Bindungen in Berechnungsausdrücken aufgerufen.
While (unit -> bool) * Delayed<'T> -> M<'T>oder

(unit -> bool) * Delayed<unit> -> M<unit>
Wird für while...do-Ausdrücke in Berechnungsausdrücken aufgerufen.
Yield 'T -> M<'T> Wird für yield-Ausdrücke in Berechnungsausdrücken aufgerufen.
YieldFrom M<'T> -> M<'T> Wird für yield!-Ausdrücke in Berechnungsausdrücken aufgerufen.
Zero unit -> M<'T> Wird für leere else-Verzweigungen von if...then-Ausdrücken in Berechnungsausdrücken aufgerufen.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Gibt an, dass der Berechnungsausdruck als Zitat an den Run-Member übergeben wird. Übersetzt alle Instanzen einer Berechnung in ein Zitat.

In vielen der Methoden in einer Builderklasse wird ein M<'T>-Konstrukt verwendet und zurückgegeben. Dieses ist in der Regel ein separat definierter Typ und beschreibt die Art Berechnungen, die kombiniert werden, z. B. Async<'T> für asynchrone Ausdrücke und Seq<'T> für Sequenzworkflows. Die Konstrukte können anhand der Signaturen dieser Methoden kombiniert und ineinander geschachtelt werden, sodass das von einem Konstrukt zurückgegebene Workflowobjekt an das nächste übergeben werden kann.

Viele Funktionen verwenden das Ergebnis von Delay als Argument: Run, While, TryWith, TryFinally und Combine. Der Typ Delayed<'T> ist der Rückgabetyp von Delay und folglich der Parameter für diese Funktionen. Delayed<'T> kann ein beliebiger Typ sein, der nicht mit M<'T> in Beziehung stehen muss; häufig werden M<'T> oder (unit -> M<'T>) verwendet. Die Standardimplementierung ist M<'T>. Detailliertere Informationen finden Sie hier.

Beim Parsen eines Berechnungsausdrucks übersetzt der Compiler den Ausdruck mithilfe der Methoden in der vorherigen Tabelle und des Codes im Berechnungsausdruck in eine Reihe geschachtelter Funktionsaufrufe. Der geschachtelte Ausdruck weist die folgende Form auf:

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

Im obigen Code werden die Aufrufe von Run und Delay weggelassen, wenn sie in der Builderklasse für den Berechnungsausdruck nicht definiert sind. Der Textkörper des Berechnungsausdrucks, hier als {{ cexpr }} angegeben, wird in weitere Aufrufe der Methoden der Generatorklasse übersetzt. Dieser Prozess wird rekursiv gemäß den Übersetzungen in der folgenden Tabelle definiert. Code in doppelten Klammern {{ ... }} bleibt zu übersetzen, expr stellt einen F#-Ausdruck dar und cexpr stellt einen Berechnungsausdruck dar.

Ausdruck Sprachübersetzung
{{ 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()

In der obigen Tabelle beschreibt other-expr einen Ausdruck, der nicht anderweitig in der Tabelle aufgeführt ist. Eine Builderklasse muss weder alle Methoden implementieren noch alle in der obigen Tabelle aufgeführten Übersetzungen unterstützen. Diese nicht implementierten Konstrukte sind in Berechnungsausdrücken dieses Typs nicht verfügbar. Wenn Sie beispielsweise das Schlüsselwort use in Ihren Berechnungsausdrücken nicht unterstützen möchten, können Sie die Definition von Use in der Builderklasse weglassen.

Das folgende Codebeispiel zeigt einen Berechnungsausdruck, der eine Berechnung als Reihe von Schritten kapselt, die jeweils einzeln ausgewertet werden können. Ein diskriminierter Union-Typ, OkOrException, codiert den Fehlerzustand des Ausdrucks, so weit er bisher ausgewertet ist. Dieser Code zeigt mehrere typische Muster, die Sie in Ihren Berechnungsausdrücken verwenden können, wie z. B. Bausteinimplementierungen einiger Buildermethoden.

/// 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

Ein Berechnungsausdruck weist einen zugrunde liegenden Typ auf, den der Ausdruck zurückgibt. Der zugrunde liegende Typ kann ein berechnetes Ergebnis oder eine verzögerte Berechnung darstellen, die ausgeführt werden kann, oder er kann eine Möglichkeit zum Durchlaufen eines bestimmten Auflistungstyps bieten. Im vorherigen Beispiel war der zugrunde liegende Typ Eventually<_>. Für einen Sequenzausdruck ist der zugrunde liegende Typ System.Collections.Generic.IEnumerable<T>. Für einen Abfrageausdruck ist der zugrunde liegende Typ System.Linq.IQueryable. Für einen asynchronen Ausdruck ist der zugrunde liegende Typ Async. Das Async-Objekt stellt die Verarbeitung dar, die ausgeführt werden muss, um das Ergebnis zu berechnen. Sie rufen z. B. Async.RunSynchronously auf, um eine Berechnung auszuführen und das Ergebnis zurückzugeben.

Benutzerdefinierte Vorgänge

Sie können einen benutzerdefinierten Vorgang in einem Berechnungsausdruck definieren und als Operator verwenden. Beispielsweise können Sie einen Abfrageoperator in einen Abfrageausdruck einschließen. Wenn Sie einen benutzerdefinierten Vorgang definieren, müssen Sie die Methoden „Yield“ und „For“ im Berechnungsausdruck definieren. Um einen benutzerdefinierten Vorgang zu definieren, fügen Sie ihn in eine Builderklasse für den Berechnungsausdruck ein, und wenden Sie dann das CustomOperationAttribute an. Dieses Attribut akzeptiert eine Zeichenfolge als Argument, bei der es sich um den Namen handelt, der in einem benutzerdefinierten Vorgang verwendet werden soll. Dieser Name wird am Anfang der öffnenden geschweiften Klammer des Berechnungsausdrucks in den Bereich eingefügt. Daher sollten Sie keine Bezeichner verwenden, die denselben Namen wie ein benutzerdefinierter Vorgang in diesem Block haben. Vermeiden Sie beispielsweise die Verwendung von Bezeichnern wie all oder last in Abfrageausdrücken.

Erweitern vorhandener Builder mit neuen benutzerdefinierten Vorgängen

Wenn Sie bereits über eine Builderklasse verfügen, können die benutzerdefinierten Vorgänge von außerhalb dieser Builderklasse erweitert werden. Erweiterungen müssen in Modulen deklariert werden. Namespaces dürfen keine Erweiterungsmember enthalten, außer in derselben Datei und in derselben Namespacedeklarationsgruppe, in der der Typ definiert ist.

Das folgende Beispiel zeigt die Erweiterung der vorhandenen FSharp.Linq.QueryBuilder-Klasse.

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

Benutzerdefinierte Vorgänge können überladen werden. Weitere Informationen finden Sie unter F# RFC FS-1056 – Allow overloads of custom keywords in computation expressions.

Effizientes Kompilieren von Berechnungsausdrücken

F#-Berechnungsausdrücke, die die Ausführung anhalten, können durch umsichtige Verwendung eines Features auf niedriger Ebene, das als wiederaufnehmbarer Code bezeichnet wird, in hocheffiziente Zustandsautomaten kompiliert werden. Diese Art von Code ist in F# RFC FS-1087 dokumentiert und wird für Aufgabenausdrücke verwendet.

F#-Berechnungsausdrücke, die synchron sind (d. h. die Ausführung nicht anhalten), können alternativ mithilfe von Inlinefunktionen einschließlich des InlineIfLambda-Attributs in effiziente Zustandsautomaten kompiliert werden. Beispiele finden Sie in F# RFC FS-1098.

Listenausdrücke, Arrayausdrücke und Sequenzausdrücke werden vom F#-Compiler auf spezielle Weise verarbeitet, um die Generierung von hochleistungsfähigem Code sicherzustellen.

Weitere Informationen