Condividi tramite


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 computazione, possono essere considerati come un modo per esprimere monadi, monoidi, trasformatori di monadi e funtori applicativi. Tuttavia, a differenza di altri linguaggi (ad esempio la do-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.

Informazioni generali

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 contestuale può risultare complessa, poiché è facile che calcoli "trasparano" all'esterno di un determinato contesto senza astrazioni che lo impediscano. 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 è zucchero sintattico per l'uso di let! seguito da un confronto del 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 sul tipo di generatore Bind(x, f).

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 let! ... let! ... forza la riesecuzione delle associazioni dispendiose, quindi let! ... and! ... dovrebbe essere utilizzato quando si associano i risultati di numerose espressioni di calcolo.

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

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 parola chiave do! serve per chiamare un'espressione di calcolo che restituisce un tipo simile a unit, definito dal membro Zero nel costruttore.

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 Bind(x, f) nel tipo generatore, dove f produce un 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 del tipo di generatore Yield(x), 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! restituirà gli elementi uno alla volta, appiattendo il risultato.

yield! è definito dal membro YieldFrom(x) nel 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 incapsula 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 Return(x) sul tipo generatore, dove x è l'elemento da avvolgere. L'utilizzo di let! ... return permette di usare BindReturn(x, f) per migliorare le prestazioni.

return!

La return! parola chiave realizza il valore di un'espressione di calcolo e racchiude quel risultato nel 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 del tipo costruttore ReturnFrom(x), in cui x è un'altra espressione di calcolo.

match!

La match! parola chiave consente di inserire in linea una chiamata a un'altra espressione di calcolo e di eseguire un pattern matching sul risultato:

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

Quando si chiama un'espressione di calcolo con match!, realizzerà il risultato della chiamata come let!. 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> Richiesto per espressioni di calcolo efficienti let! e and! senza unione di input.

ad esempio , Bind3Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Incapsula un'espressione computazionale 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> Richiesto un utilizzo efficiente di let! ... return nelle espressioni di calcolo.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Chiamato per un'implementazione efficiente let! ... and! ... return nelle espressioni di calcolo senza combinare gli input.

ad esempio , Bind3ReturnBind4Return.
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> Viene chiamato per and! nelle espressioni di calcolo, ma migliora l'efficienza riducendo il numero di nodi di accoppiamento.

ad esempio , MergeSources3MergeSources4.
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>>
Richiamato per le espressioni for...do nelle espressioni di calcolo.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Richiamato per le espressioni try...finally nelle espressioni di calcolo.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Richiamato per le espressioni try...with nelle espressioni di calcolo.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Chiamato le associazioni per use nelle espressioni di calcolo.
While (unit -> bool) * Delayed<'T> -> M<'T>o

(unit -> bool) * Delayed<unit> -> M<unit>
Richiamato per le espressioni while...do nelle espressioni di calcolo.
Yield 'T -> M<'T> Richiamato per le espressioni yield nelle espressioni di calcolo.
YieldFrom M<'T> -> M<'T> Richiamato per le espressioni yield! nelle espressioni di calcolo.
Zero unit -> M<'T> Chiamato per i rami vuoti delle espressioni in espressioni di calcolo.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Indica che l'espressione di calcolo viene passata al membro Run come citazione. 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 signature di questi metodi consentono di combinarle e annidarle tra loro, in modo che l'oggetto di flusso di lavoro restituito da un costrutto possa essere passato a quello successivo.

Molte funzioni usano il risultato di Delay come argomento: Run, While, TryWithTryFinally, e Combine. Il Delayed<'T> tipo è il tipo restituito da Delay e di conseguenza il parametro di 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 Informazioni sui vincoli di tipo.

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.

Espressione 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 Costruttori 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 vengono illustrate le estensioni della classe esistente FSharp.Linq.QueryBuilder .

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

Le operazioni personalizzate possono essere sovraccaricate. Per ulteriori informazioni, vedere F# RFC FS-1056 - Consentire il sovraccarico delle parole chiave personalizzate nelle espressioni computazionali.

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, matrice e sequenza vengono trattate in modo speciale dal compilatore F# per garantire la generazione di codice dalle alte prestazioni.

Vedere anche