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í unit
typ 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ř. Bind3 Bind4 |
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ř. Bind3Return Bind4Return |
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ř. MergeSources3 MergeSources4 |
Run |
Delayed<'T> -> M<'T> neboM<'T> -> 'T |
Spustí výpočetní výraz. |
Combine |
M<'T> * Delayed<'T> -> M<'T> neboM<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> neboseq<'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 Async
podkladový 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.