Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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 det handlar om, kan de betraktas som ett sätt att uttrycka monader, monoider, monadtransformatorer och applicativa funktorer. 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. Dock är inte alla former av beräkningar 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äknade uttrycket. 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 effektivare. Det här nyckelordet möjliggör applicativa beräkningsuttryck, som ger en annan beräkningsmodell än den vanliga monadiska metoden.
let doThingsAsync url =
async {
let! data = getDataAsync url
and! moreData = getMoreDataAsync anotherUrl
and! evenMoreData = getEvenMoreDataAsync someUrl
...
}
Om du använder en serie let! ... let! ... med körs beräkningen sekventiellt, även om de är oberoende. Indikerar däremot let! ... and! ... att beräkningarna är oberoende, vilket tillåter applicativ kombination. Med det här oberoendet kan beräkningsuttrycksförfattare:
- Köra beräkningar mer effektivt.
- Kan köra beräkningar parallellt.
- Ackumulera resultat utan onödiga sekventiella beroenden.
Begränsningen är att beräkningar i kombination med and! inte kan bero på resultatet av tidigare bundna värden i samma let!/and! kedja. Den här kompromissen möjliggör prestandafördelarna.
and! definieras främst av MergeSources(x1, x2) medlemmen på builder-typen.
Alternativt kan MergeSourcesN(x1, x2 ..., xN) definieras för att minska antalet tupling-noder, och BindN(x1, x2 ..., xN, f) eller BindNReturn(x1, x2, ..., xN, f) kan definieras för att effektivt binda resultatet av beräkningsuttryck utan tupling-noder.
Mer information om applicativa beräkningsuttryck finns i Applicative Computation Expressions in F# 5 and F# RFC FS-1063.
do!
Nyckelordet do! är för att anropa ett beräkningsuttryck som returnerar en typ som liknar unit (definierad av Zero medlemmen hos 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 räcka med 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 kommer beräkningsuttrycket som anropas av yield! att ge tillbaka sina element en i taget, vilket gör att resultatet blir plattat ut.
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) medlemsvariabeln på byggartypen, där x är objektet som ska omslutas. För användning av let! ... return, kan BindReturn(x, f) användas för bättre prestanda.
return!
Nyckelordet return! tillhandahåller värdet av ett beräkningsuttryck och omsluter resultatet i 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.till exempel 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.till exempel 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.till exempel MergeSources3, MergeSources4. |
Run |
Delayed<'T> -> M<'T> ellerM<'T> -> 'T |
Kör ett beräkningsuttryck. |
Combine |
M<'T> * Delayed<'T> -> M<'T> ellerM<unit> * M<'T> -> M<'T> |
Anropad för sekvensering i beräkningsuttryck. |
For |
seq<'T> * ('T -> M<'U>) -> M<'U> ellerseq<'T> * ('T -> M<'U>) -> seq<M<'U>> |
Anrop för for...do uttryck i beräkningssamband. |
TryFinally |
Delayed<'T> * (unit -> unit) -> M<'T> |
Anrop för try...finally uttryck i beräkningssamband. |
TryWith |
Delayed<'T> * (exn -> M<'T>) -> M<'T> |
Anrop för try...with uttryck i beräkningssamband. |
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> |
Anrop för while...do uttryck i beräkningssamband. |
Yield |
'T -> M<'T> |
Anrop för yield uttryck i beräkningssamband. |
YieldFrom |
M<'T> -> M<'T> |
Anrop för yield! uttryck i beräkningssamband. |
Zero |
unit -> M<'T> |
Behövs för tomma else grenar av if...then 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>. En mer djupgående titt finns i Förstå typbegränsningarna.
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 form:
builder.Run(builder.Delay(fun () -> {{ cexpr }}))
I koden ovan utelämnas anropen till Run och Delay om de inte definieras i byggerklassen för beräkninguttryck. 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 unionstyp, OkOrException, representerar feltillståndet för uttrycket som har 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 träder in i omfånget vid början av den första 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äggen för den befintliga FSharp.Linq.QueryBuilder klassen.
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))
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 tillfälligt avbryter körningen kan kompileras till mycket effektiva tillståndsmaskiner genom noggrann användning av en lågnivåteknik som kallas återupptagbar 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åndsmaskiner 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.