Espressioni di calcolo

Le espressioni di calcolo in F# offrono una sintassi pratica per la scrittura di calcoli che possono essere sequenziati e combinati usando costrutti e associazioni del flusso di controllo. A seconda del tipo di espressione di calcolo, possono essere considerati come un modo per esprimere monadi, monoidi, trasformatori monaci e funtori applicativi. Tuttavia, a differenza di altri linguaggi (ad esempio la notazione in Haskell), non sono associati a una singola astrazione e non si basano su macro o altre forme di metaprogrammazione per ottenere una sintassi comoda e sensibile al contesto.

Panoramica

I calcoli possono assumere molte forme. La forma più comune di calcolo è l'esecuzione a thread singolo, che è facile da comprendere e modificare. Tuttavia, non tutte le forme di calcolo sono semplici come l'esecuzione a thread singolo. Alcuni esempi includono:

  • Calcoli non deterministici
  • Calcoli asincroni
  • Calcoli con effetto
  • Calcoli generativi

In genere, esistono calcoli sensibili al contesto che è necessario eseguire in determinate parti di un'applicazione. La scrittura di codice sensibile al contesto può risultare complessa, perché è facile "perdere" calcoli all'esterno di un determinato contesto senza astrazioni per evitare di farlo. Queste astrazioni sono spesso difficili da scrivere manualmente, motivo per cui F# ha un modo generalizzato per eseguire queste espressioni di calcolo.

Le espressioni di calcolo offrono una sintassi uniforme e un modello di astrazione per la codifica dei calcoli sensibili al contesto.

Ogni espressione di calcolo è supportata da un tipo di generatore . Il tipo di generatore definisce le operazioni disponibili per l'espressione di calcolo. Vedere Creazione di un nuovo tipo di espressione di calcolo, che illustra come creare un'espressione di calcolo personalizzata.

Panoramica della sintassi

Tutte le espressioni di calcolo hanno il formato seguente:

builder-expr { cexper }

In questo formato è builder-expr il nome di un tipo di generatore che definisce l'espressione di calcolo ed cexper è il corpo dell'espressione di calcolo. Ad esempio, async il codice dell'espressione di calcolo può essere simile al seguente:

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

        let processedData = processData data

        return processedData
    }

Esiste una sintassi speciale aggiuntiva disponibile all'interno di un'espressione di calcolo, come illustrato nell'esempio precedente. I seguenti moduli di espressione sono possibili con le espressioni di calcolo:

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

Ognuna di queste parole chiave e altre parole chiave F# standard sono disponibili solo in un'espressione di calcolo se sono state definite nel tipo di generatore di supporto. L'unica eccezione a questo è match!, che è stesso zucchero sintattico per l'uso di let! seguito da una corrispondenza del criterio sul risultato.

Il tipo di generatore è un oggetto che definisce metodi speciali che regolano la modalità di combinazione dei frammenti dell'espressione di calcolo; ovvero i metodi controllano il comportamento dell'espressione di calcolo. Un altro modo per descrivere una classe builder consiste nel dire che consente di personalizzare l'operazione di molti costrutti F#, ad esempio cicli e associazioni.

let!

La let! parola chiave associa il risultato di una chiamata a un'altra espressione di calcolo a un nome:

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

Se si associa la chiamata a un'espressione di calcolo con let, non si otterrà il risultato dell'espressione di calcolo. Al contrario, si avrà associato il valore della chiamata non realizzata a tale espressione di calcolo. Utilizzare let! per eseguire l'associazione al risultato.

let! è definito dal membro nel Bind(x, f) tipo di generatore.

and!

La and! parola chiave consente di associare i risultati di più chiamate di espressione di calcolo in modo efficiente.

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

L'uso di una serie di forze esegue nuovamente let! ... let! ... le associazioni dispendiose, pertanto è consigliabile usare quando let! ... and! ... si associano i risultati di numerose espressioni di calcolo.

and! viene definito principalmente dal membro nel MergeSources(x1, x2) tipo di generatore.

Facoltativamente, MergeSourcesN(x1, x2 ..., xN) è possibile definire per ridurre il numero di nodi di tupling e BindN(x1, x2 ..., xN, f), oppure BindNReturn(x1, x2, ..., xN, f) per associare i risultati dell'espressione di calcolo in modo efficiente senza i nodi di tupling.

do!

La do! parola chiave è per chiamare un'espressione di calcolo che restituisce un unittipo simile a ,definito dal Zero membro nel generatore:

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

Per il flusso di lavoro asincrono, questo tipo è Async<unit>. Per altre espressioni di calcolo, è probabile che il tipo sia CExpType<unit>.

do! è definito dal membro nel tipo di Bind(x, f) generatore, dove f produce un oggetto unit.

yield

La yield parola chiave è per restituire un valore dall'espressione di calcolo in modo che possa essere utilizzata come IEnumerable<T>:

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

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

Nella maggior parte dei casi, può essere omesso dai chiamanti. Il modo più comune per omettere yield consiste nell'operatore -> :

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

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

Per espressioni più complesse che potrebbero produrre molti valori diversi e, ad esempio, in modo condizionale, è sufficiente omettere la parola chiave:

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

Come per la parola chiave yield in C#, ogni elemento nell'espressione di calcolo viene restituito mentre viene iterato.

yield è definito dal membro nel Yield(x) tipo di generatore, dove x è l'elemento da restituire.

yield!

La yield! parola chiave è per rendere flat una raccolta di valori da un'espressione di calcolo:

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

Quando viene valutata, l'espressione di calcolo chiamata da yield! avrà gli elementi restituiti uno per uno, appiattindo il risultato.

yield! è definito dal membro nel YieldFrom(x) tipo di generatore, dove x è una raccolta di valori.

A differenza di yield, yield! deve essere specificato in modo esplicito. Il comportamento non è implicito nelle espressioni di calcolo.

return

La return parola chiave esegue il wrapping di un valore nel tipo corrispondente all'espressione di calcolo. Oltre alle espressioni di calcolo che usano yield, viene usato per "completare" un'espressione di calcolo:

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 è definito dal membro nel tipo di Return(x) generatore, dove x è l'elemento a capo. Per let! ... return l'utilizzo, BindReturn(x, f) può essere usato per migliorare le prestazioni.

return!

La return! parola chiave realizza il valore di un'espressione di calcolo ed esegue il wrapping del tipo corrispondente all'espressione di calcolo:

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

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

return! è definito dal membro nel ReturnFrom(x) tipo di generatore, dove x è un'altra espressione di calcolo.

match!

La match! parola chiave consente di inline una chiamata a un'altra espressione di calcolo e di trovare una corrispondenza del criterio per il risultato:

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

Quando si chiama un'espressione di calcolo con match!, il risultato della chiamata è simile let!a . Questa operazione viene spesso utilizzata quando si chiama un'espressione di calcolo in cui il risultato è facoltativo.

Espressioni di calcolo predefinite

La libreria di base F# definisce quattro espressioni di calcolo predefinite: espressioni di sequenza, espressioni asincrone, espressioni task ed espressioni di query.

Creazione di un nuovo tipo di espressione di calcolo

È possibile definire le caratteristiche delle proprie espressioni di calcolo creando una classe builder e definendo determinati metodi speciali nella classe . La classe builder può facoltativamente definire i metodi elencati nella tabella seguente.

Nella tabella seguente vengono descritti i metodi che possono essere usati in una classe del generatore di flussi di lavoro.

Metodo Firme tipiche Descrizione
Bind M<'T> * ('T -> M<'U>) -> M<'U> Chiamato per let! e do! nelle espressioni di calcolo.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Chiamato per espressioni di calcolo efficienti let! e and! in caso di unione di input.

ad esempio Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Esegue il wrapping di un'espressione di calcolo come funzione. Delayed<'T> può essere qualsiasi tipo, comunemente M<'T> o unit -> M<'T> usato. L'implementazione predefinita restituisce un oggetto M<'T>.
Return 'T -> M<'T> Chiamato per return nelle espressioni di calcolo.
ReturnFrom M<'T> -> M<'T> Chiamato per return! nelle espressioni di calcolo.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Chiamato per un'efficienza let! ... return nelle espressioni di calcolo.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Chiamato per un'efficienza let! ... and! ... return nelle espressioni di calcolo senza unire input.

ad esempio Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> Chiamato per and! nelle espressioni di calcolo.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Chiamato per and! nelle espressioni di calcolo, ma migliora l'efficienza riducendo il numero di nodi di tupling.

ad esempio MergeSources3, MergeSources4.
Run Delayed<'T> -> M<'T> oppure

M<'T> -> 'T
Esegue un'espressione di calcolo.
Combine M<'T> * Delayed<'T> -> M<'T> oppure

M<unit> * M<'T> -> M<'T>
Chiamato per la sequenziazione nelle espressioni di calcolo.
For seq<'T> * ('T -> M<'U>) -> M<'U> oppure

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Chiamato per for...do le espressioni nelle espressioni di calcolo.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Chiamato per try...finally le espressioni nelle espressioni di calcolo.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Chiamato per try...with le espressioni nelle espressioni di calcolo.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Chiamato per use le associazioni nelle espressioni di calcolo.
While (unit -> bool) * Delayed<'T> -> M<'T>O

(unit -> bool) * Delayed<unit> -> M<unit>
Chiamato per while...do le espressioni nelle espressioni di calcolo.
Yield 'T -> M<'T> Chiamato per yield le espressioni nelle espressioni di calcolo.
YieldFrom M<'T> -> M<'T> Chiamato per yield! le espressioni nelle espressioni di calcolo.
Zero unit -> M<'T> Chiamato per rami vuoti else di espressioni nelle espressioni di if...then calcolo.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Indica che l'espressione di calcolo viene passata al Run membro come virgoletta. Converte tutte le istanze di un calcolo in un'offerta.

Molti dei metodi in una classe builder usano e restituiscono un M<'T> costrutto, che in genere è un tipo definito separatamente che caratterizza il tipo di calcoli combinati, ad esempio per Async<'T> le espressioni asincrone e Seq<'T> per i flussi di lavoro di sequenza. Le firme di questi metodi consentono di combinarle e annidate tra loro, in modo che l'oggetto flusso di lavoro restituito da un costrutto possa essere passato al successivo.

Molte funzioni usano il risultato di Delay come argomento: Run, While, TryWithTryFinally, e Combine. Il Delayed<'T> tipo è il tipo restituito di Delay e di conseguenza il parametro a queste funzioni. Delayed<'T> può essere un tipo arbitrario che non deve essere correlato a M<'T>; in genere M<'T> o (unit -> M<'T>) vengono usati. L'implementazione predefinita è M<'T>. Per un aspetto più approfondito, vedere qui .

Quando analizza un'espressione di calcolo, il compilatore converte l'espressione in una serie di chiamate di funzione annidate usando i metodi nella tabella precedente e il codice nell'espressione di calcolo. L'espressione nidificata è del formato seguente:

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

Nel codice precedente, le chiamate a Run e Delay vengono omesse se non sono definite nella classe generatore di espressioni di calcolo. Il corpo dell'espressione di calcolo, indicato come {{ cexpr }}, viene convertito in ulteriori chiamate ai metodi della classe builder. Questo processo viene definito in modo ricorsivo in base alle traduzioni nella tabella seguente. Il codice tra parentesi {{ ... }} doppie rimane da tradurre, expr rappresenta un'espressione F# e cexpr rappresenta un'espressione di calcolo.

Expression Traduzione
{{ 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()

Nella tabella other-expr precedente viene descritta un'espressione che non è altrimenti elencata nella tabella. Una classe builder non deve implementare tutti i metodi e supportare tutte le traduzioni elencate nella tabella precedente. Tali costrutti non implementati non sono disponibili nelle espressioni di calcolo di tale tipo. Ad esempio, se non si vuole supportare la use parola chiave nelle espressioni di calcolo, è possibile omettere la definizione di Use nella classe builder.

Nell'esempio di codice seguente viene illustrata un'espressione di calcolo che incapsula un calcolo come una serie di passaggi che possono essere valutati un passaggio alla volta. Un tipo di unione discriminato, OkOrException, codifica lo stato di errore dell'espressione come valutato finora. Questo codice illustra diversi modelli tipici che è possibile usare nelle espressioni di calcolo, ad esempio implementazioni boilerplate di alcuni metodi del generatore.

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

Un'espressione di calcolo ha un tipo sottostante, che l'espressione restituisce. Il tipo sottostante può rappresentare un risultato calcolato o un calcolo ritardato che può essere eseguito oppure può fornire un modo per scorrere un determinato tipo di raccolta. Nell'esempio precedente il tipo sottostante era Eventually<_>. Per un'espressione di sequenza, il tipo sottostante è System.Collections.Generic.IEnumerable<T>. Per un'espressione di query, il tipo sottostante è System.Linq.IQueryable. Per un'espressione asincrona, il tipo sottostante è Async. L'oggetto Async rappresenta il lavoro da eseguire per calcolare il risultato. Ad esempio, si chiama Async.RunSynchronously per eseguire un calcolo e restituire il risultato.

Operazioni personalizzate

È possibile definire un'operazione personalizzata in un'espressione di calcolo e usare un'operazione personalizzata come operatore in un'espressione di calcolo. Ad esempio, è possibile includere un operatore di query in un'espressione di query. Quando si definisce un'operazione personalizzata, è necessario definire i metodi Yield e For nell'espressione di calcolo. Per definire un'operazione personalizzata, inserirla in una classe builder per l'espressione di calcolo e quindi applicare .CustomOperationAttribute Questo attributo accetta una stringa come argomento, ovvero il nome da usare in un'operazione personalizzata. Questo nome entra nell'ambito all'inizio della parentesi graffa di apertura dell'espressione di calcolo. Pertanto, non è consigliabile usare identificatori con lo stesso nome di un'operazione personalizzata in questo blocco. Ad esempio, evitare l'uso di identificatori come all o last nelle espressioni di query.

Estensione dei generatori esistenti con nuove operazioni personalizzate

Se si dispone già di una classe builder, le relative operazioni personalizzate possono essere estese dall'esterno di questa classe builder. Le estensioni devono essere dichiarate nei moduli. Gli spazi dei nomi non possono contenere membri di estensione tranne nello stesso file e nello stesso gruppo di dichiarazione dello spazio dei nomi in cui è definito il tipo.

Nell'esempio seguente viene illustrata l'estensione della classe esistente FSharp.Linq.QueryBuilder .

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

È possibile eseguire l'overload delle operazioni personalizzate. Per altre informazioni, vedere F# RFC FS-1056 - Consenti overload di parole chiave personalizzate nelle espressioni di calcolo.

Compilazione efficiente delle espressioni di calcolo

Le espressioni di calcolo F# che sospendono l'esecuzione possono essere compilate in macchine a stati altamente efficienti tramite un'attenta uso di una funzionalità di basso livello denominata codice ripristinabile. Il codice ripristinabile è documentato in F# RFC FS-1087 e usato per le espressioni di attività.

Le espressioni di calcolo F# sincrone (ovvero non sospendono l'esecuzione) possono essere compilate in alternativa alle macchine a stati efficienti usando funzioni inline, incluso l'attributo InlineIfLambda . Gli esempi sono riportati in F# RFC FS-1098.

Le espressioni di elenco, le espressioni di matrice e le espressioni di sequenza vengono fornite dal compilatore F# per garantire la generazione di codice ad alte prestazioni.

Vedi anche