Výpočetní výrazy

Výpočetní výrazy v jazyce F# poskytují pohodlnou syntaxi pro psaní výpočtů, které lze sekvencovat a kombinovat pomocí konstruktorů a vazeb toku řízení. V závislosti na druhu výpočetního výrazu je možné je považovat za způsob vyjádření monadů, monoidů, transformátorů monadů a aplikačních funktorů. Na rozdíl od jiných jazyků (například zápisu v Haskellu) však nejsou svázané s jedinou abstrakcí a nespoléhají se na makra nebo jiné formy metaprogramování, aby bylo možné dosáhnout pohodlné a kontextově citlivé syntaxe.

Přehled

Výpočty můžou mít mnoho forem. Nejběžnější formou výpočtu je provádění s jedním vláknem, což je snadné pochopit a upravit. Ne všechny formy výpočtů jsou ale stejně jednoduché jako provádění s jedním vláknem. Mezi některé příklady patří:

  • Ne deterministické výpočty
  • Asynchronní výpočty
  • Efektní výpočty
  • Generování výpočtů

Obecně platí, že existují výpočty citlivé na kontext, které musíte provést v určitých částech aplikace. Psaní kódu citlivého na kontext může být náročné, protože je snadné "únik" výpočtů mimo daný kontext bez abstrakcí, aby vám to zabránilo. Tyto abstrakce jsou často náročné psát sami, což je důvod, proč jazyk F# má zobecněný způsob, jak to udělat, označované jako výpočetní výrazy.

Výpočetní výrazy nabízejí jednotný model syntaxe a abstrakce pro výpočty citlivé na kontext kódování.

Každý výpočetní výraz je podporován typem tvůrce . Typ tvůrce definuje operace, které jsou k dispozici pro výpočetní výraz. Viz Vytvoření nového typu výpočetního výrazu, který ukazuje, jak vytvořit vlastní výpočetní výraz.

Přehled syntaxe

Všechny výpočetní výrazy mají následující tvar:

builder-expr { cexper }

V tomto formuláři je název typu tvůrce, builder-expr který definuje výpočetní výraz a cexper je tělo výrazu výpočtu výrazu. Například async kód výpočetního výrazu může vypadat takto:

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

        let processedData = processData data

        return processedData
    }

Ve výrazu výpočtu je k dispozici speciální další syntaxe, jak je znázorněno v předchozím příkladu. Následující formuláře výrazů jsou možné s výpočetními výrazy:

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

Každé z těchto klíčových slov a další standardní klíčová slova jazyka F# jsou k dispozici pouze ve výpočetním výrazu, pokud byly definovány v typu backing builderu. Jedinou výjimkou je match!, což je sám syntaktický cukr pro použití následované let! vzorovou shodu na výsledku.

Typ tvůrce je objekt, který definuje speciální metody, které řídí způsob kombinování fragmentů výpočetního výrazu; to znamená, že jeho metody řídí chování výpočetního výrazu. Dalším způsobem, jak popsat třídu tvůrce, je říct, že umožňuje přizpůsobit operace mnoha konstruktorů jazyka F#, jako jsou smyčky a vazby.

let!

Klíčové let! slovo sváže výsledek volání na jiný výpočetní výraz s názvem:

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

Pokud svážete volání s výpočetním výrazem let, nedostanete výsledek výpočetního výrazu. Místo toho budete mít vázanou hodnotu nerealizovaného volání na tento výpočetní výraz. Slouží let! k vytvoření vazby k výsledku.

let! je definován členem Bind(x, f) typu tvůrce.

and!

Klíčové and! slovo umožňuje svázat výsledky více volání výpočetních výrazů výkonným způsobem.

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

Použití řady let! ... let! ... sil opětovného spuštění drahých vazeb, takže použití let! ... and! ... by se mělo použít při vazbě výsledků mnoha výpočetních výrazů.

and! je definován především MergeSources(x1, x2) členem typu tvůrce.

Volitelně můžete definovat, aby se snížil počet uzlů řazených kolekcí členů, BindN(x1, x2 ..., xN, f)nebo BindNReturn(x1, x2, ..., xN, f) je možné je definovat tak, MergeSourcesN(x1, x2 ..., xN) aby efektivně svážely výsledky výpočetních výrazů bez řazených uzlů.

do!

Klíčové do! slovo je pro volání výpočetního výrazu, který vrací unittyp like (definovaný Zero členem v tvůrci):

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

Pro asynchronní pracovní postup je Async<unit>tento typ . U jiných výpočetních výrazů bude typ pravděpodobně CExpType<unit>.

do!je definován členem Bind(x, f) typu tvůrce, kde f vytvoří .unit

yield

Klíčové yield slovo je pro vrácení hodnoty z výpočetního výrazu, aby bylo možné ho použít jako IEnumerable<T>:

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

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

Ve většině případů ho můžou volající vynechat. Nejběžnější způsob, jak vynechat yield , je operátor -> :

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

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

U složitějších výrazů, které by mohly přinést mnoho různých hodnot, a možná podmíněně, stačí, když klíčové slovo vynecháte:

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

Stejně jako u klíčového slova yield v jazyce C# se každý prvek ve výpočetním výrazu vrátí zpět, protože se iterated.

yield je definován Yield(x) členem typu tvůrce, kde x je položka, která se má vrátit zpět.

yield!

Klíčové yield! slovo je pro zploštění kolekce hodnot z výpočetního výrazu:

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

Při vyhodnocení bude výpočetní výraz volaný yield! podle obsahovat jeho položky, které vrátí jeden po druhém, což zploštělo výsledek.

yield! je definován členem YieldFrom(x) typu tvůrce, kde x je kolekce hodnot.

Na rozdíl od yield, yield! musí být explicitně zadán. Jeho chování není implicitní ve výpočetních výrazech.

return

Klíčové return slovo zabalí hodnotu v typu odpovídající výpočetnímu výrazu. Kromě výpočetních výrazů, které používají yield, se používá k "dokončení" výpočetního výrazu:

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 je definován členem Return(x) typu tvůrce, kde je položka, která x se má zabalit. Pro let! ... return účely využití BindReturn(x, f) je možné použít ke zlepšení výkonu.

return!

Klíčové return! slovo si uvědomí hodnotu výpočetního výrazu a zalomí výsledek typu odpovídající výpočtu výrazu:

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

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

return! je definován členem ReturnFrom(x) typu tvůrce, kde x je další výpočetní výraz.

match!

Klíčové match! slovo umožňuje vložit volání jiného výpočetního výrazu a shody vzorů ve výsledku:

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

Při volání výpočetního výrazu s match!, zjistí výsledek volání jako let!. Často se používá při volání výpočetního výrazu, kde je výsledek nepovinný.

Předdefinované výpočetní výrazy

Základní knihovna jazyka F# definuje čtyři předdefinované výpočetní výrazy: sequence Expressions, Async expressions, Task expressions a Query Expressions.

Vytvoření nového typu výpočetního výrazu

Vlastnosti vlastních výpočetních výrazů můžete definovat vytvořením třídy tvůrce a definováním určitých speciálních metod třídy. Třída tvůrce může volitelně definovat metody uvedené v následující tabulce.

Následující tabulka popisuje metody, které lze použít ve třídě tvůrce pracovních postupů.

Metoda Typické podpisy Popis
Bind M<'T> * ('T -> M<'U>) -> M<'U> let! Volali jsme a do! v výpočetních výrazech.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Volali jsme pro efektivní let! a and! ve výpočetních výrazech bez sloučení vstupů.

např. Bind3Bind4
Delay (unit -> M<'T>) -> Delayed<'T> Zabalí výpočetní výraz jako funkci. Delayed<'T> může být libovolný typ, běžně M<'T> nebo unit -> M<'T> se používá. Výchozí implementace vrátí M<'T>hodnotu .
Return 'T -> M<'T> return Volá se ve výpočetních výrazech.
ReturnFrom M<'T> -> M<'T> return! Volá se ve výpočetních výrazech.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Volali jsme pro efektivní let! ... return výpočty ve výpočetních výrazech.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Volali jsme pro efektivní let! ... and! ... return výpočetní výrazy bez sloučení vstupů.

např. Bind3ReturnBind4Return
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> and! Volá se ve výpočetních výrazech.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Volali jsme pro and! výpočetní výrazy, ale zvyšuje efektivitu snížením počtu uzlů řazených členů.

např. MergeSources3MergeSources4
Run Delayed<'T> -> M<'T> nebo

M<'T> -> 'T
Spustí výpočetní výraz.
Combine M<'T> * Delayed<'T> -> M<'T> nebo

M<unit> * M<'T> -> M<'T>
Volali jsme pro sekvencování ve výpočetních výrazech.
For seq<'T> * ('T -> M<'U>) -> M<'U> nebo

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Volá se pro for...do výrazy ve výpočetních výrazech.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Volá se pro try...finally výrazy ve výpočetních výrazech.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Volá se pro try...with výrazy ve výpočetních výrazech.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Volali jsme vazby use ve výpočetních výrazech.
While (unit -> bool) * Delayed<'T> -> M<'T>Nebo

(unit -> bool) * Delayed<unit> -> M<unit>
Volá se pro while...do výrazy ve výpočetních výrazech.
Yield 'T -> M<'T> Volá se pro yield výrazy ve výpočetních výrazech.
YieldFrom M<'T> -> M<'T> Volá se pro yield! výrazy ve výpočetních výrazech.
Zero unit -> M<'T> Volá se pro prázdné else větve výrazů if...then ve výpočetních výrazech.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Označuje, že výpočetní výraz se předá členu Run jako uvozovky. Převede všechny instance výpočtu do uvozovek.

Mnoho metod v tvůrci třídy používá a vrací M<'T> konstruktor, což je obvykle samostatně definovaný typ, který charakterizuje druh výpočtů, které se kombinují, Async<'T> například pro asynchronní výrazy a Seq<'T> pro sekvenční pracovní postupy. Podpisy těchto metod umožňují jejich kombinování a vnoření mezi sebou, aby objekt pracovního postupu vrácený z jedné konstrukce mohl být předán do další.

Mnoho funkcí používá výsledek Delay jako argument: Run, While, TryWith, TryFinally, a Combine. Typ Delayed<'T> je návratový Delay typ a v důsledku toho parametr pro tyto funkce. Delayed<'T> může být libovolný typ, který nemusí souviset s M<'T>; běžně M<'T> nebo (unit -> M<'T>) se používají. Výchozí implementace je M<'T>. Podrobnější pohled najdete tady .

Kompilátor při analýze výpočetního výrazu přeloží výraz do řady vnořených volání funkcí pomocí metod v předchozí tabulce a kódu ve výpočetním výrazu. Vnořený výraz má následující tvar:

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

Ve výše uvedeném kódu jsou volání Run a Delay jsou vynechána, pokud nejsou definovány ve třídě tvůrce výpočetních výrazů. Tělo výpočetního výrazu, zde označeno jako {{ cexpr }}, je přeloženo do dalších volání metod třídy tvůrce. Tento proces je definován rekurzivně podle překladů v následující tabulce. Kód v dvojitých závorkách {{ ... }} zůstane přeložen, expr představuje výraz jazyka F# a cexpr představuje výpočetní výraz.

Výraz Překlad
{{ 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()

V předchozí tabulce popisuje výraz, other-expr který není jinak uveden v tabulce. Třída tvůrce nemusí implementovat všechny metody a podporovat všechny překlady uvedené v předchozí tabulce. Tyto konstrukce, které nejsou implementovány, nejsou k dispozici ve výpočetních výrazech daného typu. Pokud například nechcete podporovat use klíčové slovo ve výpočetních výrazech, můžete vynechat definici Use ve třídě tvůrce.

Následující příklad kódu ukazuje výpočetní výraz, který zapouzdřuje výpočet jako řadu kroků, které lze vyhodnotit jeden krok najednou. Diskriminovaný sjednocovací typ OkOrException, kóduje stav chyby výrazu, jak je dosud vyhodnoceno. Tento kód ukazuje několik typických vzorů, které můžete použít ve výpočetních výrazech, jako jsou často používané implementace některých metod tvůrce.

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

Výpočetní výraz má základní typ, který výraz vrátí. Základní typ může představovat vypočítaný výsledek nebo zpožděný výpočet, který lze provést, nebo může poskytnout způsob, jak iterovat nějakým typem kolekce. V předchozím příkladu byl Eventually<_>základní typ . U sekvenčního výrazu je System.Collections.Generic.IEnumerable<T>podkladový typ . U výrazu dotazu je System.Linq.IQueryablepodkladový typ . Pro asynchronní výraz je Asyncpodkladový typ . Objekt Async představuje práci, která se má provést pro výpočet výsledku. Voláním například provedete Async.RunSynchronously výpočet a vrátíte výsledek.

Vlastní operace

Můžete definovat vlastní operaci pro výpočetní výraz a použít vlastní operaci jako operátor ve výpočetním výrazu. Do výrazu dotazu můžete například zahrnout operátor dotazu. Když definujete vlastní operaci, musíte definovat metody Yield a For ve výpočetním výrazu. Chcete-li definovat vlastní operaci, vložte ji do třídy tvůrce pro výpočetní výraz a pak použijte CustomOperationAttribute. Tento atribut přebírá řetězec jako argument, což je název, který se má použít ve vlastní operaci. Tento název přichází do oboru na začátku počáteční složené závorky výpočetního výrazu. Proto byste neměli používat identifikátory, které mají stejný název jako vlastní operace v tomto bloku. Vyhněte se například použití identifikátorů, jako all jsou nebo last ve výrazech dotazu.

Rozšíření existujících Builderů o nové vlastní operace

Pokud již máte třídu tvůrce, její vlastní operace lze rozšířit mimo tuto třídu tvůrce. Rozšíření musí být deklarována v modulech. Obory názvů nemohou obsahovat členy rozšíření kromě stejného souboru a stejné skupiny deklarací oboru názvů, ve které je typ definován.

Následující příklad ukazuje rozšíření existující FSharp.Linq.QueryBuilder třídy.

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

Vlastní operace je možné přetížit. Další informace najdete v tématu F# RFC FS-1056 – Povolení přetížení vlastních klíčových slov ve výpočetních výrazech.

Efektivní kompilace výpočetních výrazů

Výpočetní výrazy jazyka F#, které pozastaví provádění, je možné zkompilovat do vysoce efektivních stavových počítačů pomocí pečlivého použití funkce nízké úrovně označované jako obnovitelný kód. Obnovitelný kód je zdokumentovaný v F# RFC FS-1087 a používá se pro výrazy úloh.

Výpočetní výrazy jazyka F#, které jsou synchronní (tj. nepřestavují provádění), lze alternativně zkompilovat do efektivních stavových počítačů pomocí vložených funkcí včetně atributu InlineIfLambda . Příklady jsou uvedeny v F# RFC FS-1098.

Výrazy seznamů, výrazy pole a sekvenční výrazy mají speciální zacházení kompilátorem jazyka F#, aby se zajistilo generování vysoce výkonného kódu.

Viz také