Beräkningsuttryck

Beräkningsuttryck i F# ger en praktisk syntax för att skriva beräkningar som kan sekvenseras och kombineras med hjälp av kontrollflödeskonstruktioner och bindningar. Beroende på vilken typ av beräkningsuttryck de kan betraktas som ett sätt att uttrycka monader, monoider, monad transformatorer och applicativa functors. Men till skillnad från andra språk (till exempel do-notation i Haskell) är de inte knutna till en enda abstraktion och förlitar sig inte på makron eller andra former av metaprogrammering för att uppnå en bekväm och kontextkänslig syntax.

Översikt

Beräkningar kan ta många former. Den vanligaste formen av beräkning är enkeltrådad körning, vilket är lätt att förstå och ändra. Alla typer av beräkningar är dock inte lika enkla som entrådad körning. Vissa exempel inkluderar:

  • Icke-deterministiska beräkningar
  • Asynkrona beräkningar
  • Effektfulla beräkningar
  • Generativa beräkningar

Mer allmänt finns det kontextkänsliga beräkningar som du måste utföra i vissa delar av ett program. Det kan vara svårt att skriva sammanhangskänslig kod eftersom det är enkelt att "läcka" beräkningar utanför en viss kontext utan abstraktioner för att hindra dig från att göra det. Dessa abstraktioner är ofta svåra att skriva själv, vilket är anledningen till att F# har ett generaliserat sätt att göra så kallade beräkningsuttryck.

Beräkningsuttryck erbjuder en enhetlig syntax- och abstraktionsmodell för kodning av kontextkänsliga beräkningar.

Varje beräkningsuttryck backas upp av en builder-typ . Builder-typen definierar de åtgärder som är tillgängliga för beräkningsuttrycket. Se Skapa en ny typ av beräkningsuttryck, som visar hur du skapar ett anpassat beräkningsuttryck.

Syntaxöversikt

Alla beräkningsuttryck har följande formulär:

builder-expr { cexper }

I det här formuläret builder-expr är namnet på en builder-typ som definierar beräkningsuttrycket och cexper är uttryckstexten i beräkningsuttrycket. Beräkningsuttryckskoden async kan till exempel se ut så här:

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

        let processedData = processData data

        return processedData
    }

Det finns en särskild, ytterligare syntax som är tillgänglig i ett beräkningsuttryck, som du ser i föregående exempel. Följande uttrycksformulär är möjliga med beräkningsuttryck:

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

Vart och ett av dessa nyckelord och andra F#-standardnyckelord är endast tillgängliga i ett beräkningsuttryck om de har definierats i typ av stödverktyg. Det enda undantaget är match!, vilket i sig är syntaktiskt socker för användning av let! följt av en mönstermatchning på resultatet.

Builder-typen är ett objekt som definierar särskilda metoder som styr hur fragmenten i beräkningsuttrycket kombineras. Dess metoder styr alltså hur beräkningsuttrycket beter sig. Ett annat sätt att beskriva en builder-klass är att säga att det gör att du kan anpassa driften av många F#-konstruktioner, till exempel loopar och bindningar.

let!

Nyckelordet let! binder resultatet av ett anrop till ett annat beräkningsuttryck till ett namn:

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

Om du binder anropet till ett beräkningsuttryck med letfår du inte resultatet av beräkningsuttrycket. I stället har du bundit värdet för det orealiserade anropet till det beräkningsuttrycket. Använd let! för att binda till resultatet.

let! definieras av Bind(x, f) medlemmen på builder-typen.

and!

Med nyckelordet and! kan du binda resultatet av anrop med flera beräkningsuttryck på ett högpresterande sätt.

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

Med hjälp av let! ... let! ... en serie krafter återkörning av dyra bindningar, så användning let! ... and! ... bör användas när du binder resultatet av många beräkningsuttryck.

and! definieras främst av MergeSources(x1, x2) medlemmen på builder-typen.

MergeSourcesN(x1, x2 ..., xN) Du kan också definiera att minska antalet tupling-noder och BindN(x1, x2 ..., xN, f), eller BindNReturn(x1, x2, ..., xN, f) definieras för att binda beräkningsuttryckets resultat effektivt utan att tupling-noder.

do!

Nyckelordet do! är för att anropa ett beräkningsuttryck som returnerar en unit-like-typ (definierad av Zero medlemmen i byggaren):

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

För asynkront arbetsflöde är Async<unit>den här typen . För andra beräkningsuttryck är typen troligen CExpType<unit>.

do! definieras av Bind(x, f) medlemmen på builder-typen, där f skapar en unit.

yield

Nyckelordet yield är för att returnera ett värde från beräkningsuttrycket så att det kan användas som :IEnumerable<T>

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

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

I de flesta fall kan det utelämnas av anropare. Det vanligaste sättet att utelämna yield är med operatorn -> :

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

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

För mer komplexa uttryck som kan ge många olika värden, och kanske villkorligt, kan det göra att helt enkelt utelämna nyckelordet:

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

Precis som med nyckelordet yield i C# returneras varje element i beräkningsuttrycket när det itereras.

yield definieras av Yield(x) medlemmen på builder-typen, där x är objektet som ska returneras.

yield!

Nyckelordet yield! är för att platta ut en samling värden från ett beräkningsuttryck:

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

När det utvärderas får beräkningsuttrycket som anropas av yield! dess objekt tillbaka en i taget, vilket jämnar ut resultatet.

yield! definieras av YieldFrom(x) medlemmen på builder-typen, där x är en samling värden.

Till skillnad från yieldmåste yield! uttryckligen anges. Dess beteende är inte implicit i beräkningsuttryck.

return

Nyckelordet return omsluter ett värde i den typ som motsvarar beräkningsuttrycket. Förutom beräkningsuttryck som använder yieldanvänds det för att "slutföra" ett beräkningsuttryck:

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 definieras av Return(x) medlemmen på builder-typen, där x är objektet som ska radbryts. För let! ... return användning BindReturn(x, f) kan användas för bättre prestanda.

return!

Nyckelordet return! realiserar värdet för ett beräkningsuttryck och omsluter den typ som motsvarar beräkningsuttrycket:

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

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

return! definieras av ReturnFrom(x) medlemmen på builder-typen, där x är ett annat beräkningsuttryck.

match!

Med nyckelordet match! kan du infoga ett anrop till ett annat beräkningsuttryck och mönstermatchning på resultatet:

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

När du anropar ett beräkningsuttryck med match!, kommer det att inse resultatet av anropet som let!. Detta används ofta när du anropar ett beräkningsuttryck där resultatet är valfritt.

Inbyggda beräkningsuttryck

Kärnbiblioteket F# definierar fyra inbyggda beräkningsuttryck: sekvensuttryck, Async-uttryck, uppgiftsuttryck och frågeuttryck.

Skapa en ny typ av beräkningsuttryck

Du kan definiera egenskaperna för dina egna beräkningsuttryck genom att skapa en builder-klass och definiera vissa specialmetoder för klassen. Builder-klassen kan också definiera metoderna enligt listan i följande tabell.

I följande tabell beskrivs metoder som kan användas i en arbetsflödesbyggareklass.

Metod Typiska signaturer Beskrivning
Bind M<'T> * ('T -> M<'U>) -> M<'U> Anropade för let! och do! i beräkningsuttryck.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Efterlyste effektiva let! och and! i beräkningsuttryck utan sammanslagning av indata.

t.ex. Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Omsluter ett beräkningsuttryck som en funktion. Delayed<'T> kan vara valfri typ, ofta M<'T> eller unit -> M<'T> används. Standardimplementeringen returnerar en M<'T>.
Return 'T -> M<'T> return Anropas i beräkningsuttryck.
ReturnFrom M<'T> -> M<'T> return! Anropas i beräkningsuttryck.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Efterlyste ett effektivt let! ... return beräkningsuttryck.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Efterlyste effektiv let! ... and! ... return beräkningsuttryck utan sammanslagning av indata.

t.ex. Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> and! Anropas i beräkningsuttryck.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Kallas för and! i beräkningsuttryck, men förbättrar effektiviteten genom att minska antalet tupling-noder.

t.ex. MergeSources3, MergeSources4.
Run Delayed<'T> -> M<'T> eller

M<'T> -> 'T
Kör ett beräkningsuttryck.
Combine M<'T> * Delayed<'T> -> M<'T> eller

M<unit> * M<'T> -> M<'T>
Anropad för sekvensering i beräkningsuttryck.
For seq<'T> * ('T -> M<'U>) -> M<'U> eller

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Anropade för for...do uttryck i beräkningsuttryck.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Anropade för try...finally uttryck i beräkningsuttryck.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Anropade för try...with uttryck i beräkningsuttryck.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Anropade för use bindningar i beräkningsuttryck.
While (unit -> bool) * Delayed<'T> -> M<'T>Eller

(unit -> bool) * Delayed<unit> -> M<unit>
Anropade för while...do uttryck i beräkningsuttryck.
Yield 'T -> M<'T> Anropade för yield uttryck i beräkningsuttryck.
YieldFrom M<'T> -> M<'T> Anropade för yield! uttryck i beräkningsuttryck.
Zero unit -> M<'T> Anropade för tomma else grenar av if...then uttryck i beräkningsuttryck.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Anger att beräkningsuttrycket skickas till Run medlemmen som en offert. Den översätter alla instanser av en beräkning till en offert.

Många av metoderna i en builder-klass använder och returnerar en M<'T> konstruktion, som vanligtvis är en separat definierad typ som karakteriserar den typ av beräkningar som kombineras, Async<'T> till exempel för asynkrona uttryck och Seq<'T> för sekvensarbetsflöden. Med signaturerna för dessa metoder kan de kombineras och kapslas med varandra, så att arbetsflödesobjektet som returneras från en konstruktion kan skickas till nästa.

Många funktioner använder resultatet av Delay som ett argument: Run, While, TryWith, TryFinallyoch Combine. Typen Delayed<'T> är returtypen för Delay och därmed parametern till dessa funktioner. Delayed<'T> kan vara en godtycklig typ som inte behöver vara relaterad till M<'T>, ofta M<'T> eller (unit -> M<'T>) används. Standardimplementeringen är M<'T>. Se här för en mer djupgående titt.

När kompilatorn parsar ett beräkningsuttryck översätter uttrycket till en serie kapslade funktionsanrop med hjälp av metoderna i föregående tabell och koden i beräkningsuttrycket. Det kapslade uttrycket är av följande formulär:

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

I koden ovan utelämnas anropen till Run och Delay om de inte definieras i beräkningens uttrycks builder-klass. Brödtexten i beräkningsuttrycket, som här betecknas som {{ cexpr }}, översätts till ytterligare anrop till metoderna i builder-klassen. Den här processen definieras rekursivt enligt översättningarna i följande tabell. Kod inom dubbla hakparenteser {{ ... }} återstår att översätta, expr representerar ett F#-uttryck och cexpr representerar ett beräkningsuttryck.

Uttryck Översättning
{{ 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()

I föregående tabell other-expr beskrivs ett uttryck som annars inte visas i tabellen. En builder-klass behöver inte implementera alla metoder och stöder alla översättningar som anges i föregående tabell. De konstruktioner som inte implementeras är inte tillgängliga i beräkningsuttryck av den typen. Om du till exempel inte vill stödja nyckelordet use i dina beräkningsuttryck kan du utelämna definitionen av Use i din builder-klass.

I följande kodexempel visas ett beräkningsuttryck som kapslar in en beräkning som en serie steg som kan utvärderas ett steg i taget. En diskriminerad union typ, OkOrException, kodar feltillståndet för uttrycket som utvärderats hittills. Den här koden visar flera typiska mönster som du kan använda i dina beräkningsuttryck, till exempel implementeringar av exempel på några av byggmetoderna.

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

Ett beräkningsuttryck har en underliggande typ som uttrycket returnerar. Den underliggande typen kan representera ett beräknat resultat eller en fördröjd beräkning som kan utföras, eller så kan den ge ett sätt att iterera genom någon typ av samling. I föregående exempel var Eventually<_>den underliggande typen . För ett sekvensuttryck är System.Collections.Generic.IEnumerable<T>den underliggande typen . För ett frågeuttryck är System.Linq.IQueryableden underliggande typen . För ett asynkront uttryck är Asyncden underliggande typen . Objektet Async representerar det arbete som ska utföras för att beräkna resultatet. Du anropar Async.RunSynchronously till exempel för att köra en beräkning och returnera resultatet.

Anpassade åtgärder

Du kan definiera en anpassad åtgärd för ett beräkningsuttryck och använda en anpassad åtgärd som operator i ett beräkningsuttryck. Du kan till exempel inkludera en frågeoperator i ett frågeuttryck. När du definierar en anpassad åtgärd måste du definiera metoderna Yield och For i beräkningsuttrycket. Om du vill definiera en anpassad åtgärd placerar du den i en builder-klass för beräkningsuttrycket och tillämpar CustomOperationAttributesedan . Det här attributet tar en sträng som ett argument, vilket är namnet som ska användas i en anpassad åtgärd. Det här namnet kommer till omfånget i början av den inledande klammerparentesen för beräkningsuttrycket. Därför bör du inte använda identifierare som har samma namn som en anpassad åtgärd i det här blocket. Undvik till exempel att använda identifierare som all eller last i frågeuttryck.

Utöka befintliga byggare med nya anpassade åtgärder

Om du redan har en builder-klass kan dess anpassade åtgärder utökas utanför den här builder-klassen. Tillägg måste deklareras i moduler. Namnområden får inte innehålla tilläggsmedlemmar förutom i samma fil och samma namnområdesdeklarationsgrupp där typen har definierats.

I följande exempel visas tillägget för den befintliga FSharp.Linq.QueryBuilder klassen.

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

Anpassade åtgärder kan överbelastas. Mer information finns i F# RFC FS-1056 – Tillåt överlagring av anpassade nyckelord i beräkningsuttryck.

Kompilera beräkningsuttryck effektivt

F#-beräkningsuttryck som pausar körningen kan kompileras till mycket effektiva tillståndsdatorer genom noggrann användning av en lågnivåfunktion som kallas återanvändbar kod. Återanvändbar kod dokumenteras i F# RFC FS-1087 och används för uppgiftsuttryck.

F#-beräkningsuttryck som är synkrona (det vill säga att de inte pausar körningen) kan också kompileras till effektiva tillståndsdatorer med hjälp av infogade funktioner , inklusive InlineIfLambda attributet. Exempel ges i F# RFC FS-1098.

Listuttryck, matrisuttryck och sekvensuttryck ges särskild behandling av F#-kompilatorn för att säkerställa generering av kod med höga prestanda.

Se även