Condividi tramite


Espressioni di calcolo (F#)

Le espressioni di calcolo in F# forniscono un'utile sintassi per la scrittura di calcoli che possono essere ordinati in sequenza e combinati utilizzando costrutti e associazioni del flusso di controllo. Possono essere utilizzati per fornire un'utile sintassi per i monad, una funzionalità di programmazione funzionale che può essere utilizzata per gestire dati, controllo ed effetti collaterali nei programmi funzionali.

Flussi di lavoro incorporati

Le espressioni di sequenza sono un esempio di un'espressione di calcolo, come i flussi di lavoro asincroni e le espressioni di query. Per ulteriori informazioni, vedere le sequenze di, I flussi di lavoro asincrono, e Le espressioni di Query.

Determinate funzionalità sono comuni sia alle espressioni di sequenza che ai flussi di lavoro asincroni e illustrano la sintassi di base per un'espressione di calcolo:

builder-name { expression }

Nella sintassi precedente viene indicato che l'espressione specificata è un'espressione di calcolo di un tipo specificato da builder-name. L'espressione di calcolo può essere un flusso di lavoro incorporato, ad esempio seq o async, oppure un elemento definito dall'utente. builder-name è l'identificatore per un'istanza di un tipo speciale, noto come tipo generatore. Il tipo generatore è un tipo di classe che definisce i metodi speciali che regolano la modalità di combinazione dei frammenti dell'espressione di calcolo, ovvero il codice che controlla la modalità di esecuzione dell'espressione. Un altro modo per descrivere una classe generatore consiste nell'affermare che consente di personalizzare l'operazione di molti costrutti F#, ad esempio cicli e associazioni.

Nelle espressioni di calcolo sono disponibili due forme per alcuni costrutti di linguaggio comuni. È possibile richiamare i costrutti di tipo variant utilizzando il simbolo ! (punto esclamativo) in determinate parole chiave, ad esempio let!, do! e così via. A causa di queste forme speciali, alcune funzioni definite nella classe del generatore sostituiscono il comportamento incorporato comune di queste operazioni. Si tratta di un formato simile a yield! della parola chiave yield utilizzata nelle espressioni sequenza. Per ulteriori informazioni, vedere Sequenza.

Creazione di un nuovo tipo di espressione di calcolo

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

Nella tabella seguente sono descritti metodi che possono essere utilizzati in una classe del generatore di flusso di lavoro.

Metodo

Firma tipica

Descrizione

Bind

M<'T> * ('T -> M<'U>) -> M<'U>

Chiamato per let! e do! nelle espressioni di calcolo.

Delay

(unit -> M<'T>) -> M<'T>

Esegue il wrapping di un'espressione di calcolo come funzione.

Return

'T -> M<'T>

Chiamato per return nelle espressioni di calcolo.

ReturnFrom

M<'T> -> M<'T>

Chiamato per return! nelle espressioni di calcolo.

Run

M<'T> -> M<'T> oppure

M<'T> -> 'T

Esegue un'espressione di calcolo.

Combine

M<'T> * M<'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 le espressioni for...do nelle espressioni di calcolo.

TryFinally

M<'T> * (unit -> unit) -> M<'T>

Chiamato per le espressioni try...finally nelle espressioni di calcolo.

TryWith

M<'T> * (exn -> M<'T>) -> M<'T>

Chiamato per le espressioni try...with nelle espressioni di calcolo.

Using

'T * ('T -> M<'U>) -> M<'U> when 'U :> IDisposable

Chiamato per le associazioni use nelle espressioni di calcolo.

While

(unit -> bool) * M<'T> -> M<'T>

Chiamato per le espressioni while...do nelle espressioni di calcolo.

Yield

'T -> M<'T>

Chiamato per le espressioni yield nelle espressioni di calcolo.

YieldFrom

M<'T> -> M<'T>

Chiamato per le espressioni yield! nelle espressioni di calcolo.

Zero

unit -> M<'T>

Chiamato per i rami else vuoti delle espressioni if...then nelle espressioni di calcolo.

Molti dei metodi in una classe del generatore di utilizzare e restituire un M<'T> costrutto, che è in genere un tipo definito separatamente che caratterizza il tipo di calcoli da associare, ad esempio, Async<'T> per i flussi di lavoro asincroni e Seq<'T> per i flussi di lavoro di sequenza. Le firme consentono a questi metodi di combinarsi e annidarsi tra loro, in modo che l'oggetto del flusso di lavoro restituito da un costrutto possa essere passato al successivo. Nel momento in cui analizza un'espressione di calcolo, il compilatore converte l'espressione in una serie di chiamate di funzioni annidate utilizzando i metodi nella tabella precedente e il codice nell'espressione di calcolo.

L'espressione annidata ha il 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 del generatore di espressioni di calcolo. Il corpo dell'espressione di calcolo, qui indicato da {| cexpr |}, viene convertito nelle chiamate che includono i metodi della classe del generatore dalle conversioni descritte nella tabella riportata di seguito. L'oggetto {| cexpr |} dell'espressione di calcolo è definito in modo ricorsivo in base a queste conversioni in cui expr è un'espressione F# e cexpr è un'espressione di calcolo.

Espressione

Conversione

{| 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(expr1, (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 binder.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 expr do cexpr |}

builder.For(enumeration, (fun pattern -> {| cexpr }|))

{| for identifier = expr1 to expr2 do cexpr |}

builder.For(enumeration, (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 -> reraise exn)))

{| 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 precedente, other-expr descrive un'espressione che non è elencata in altro modo nella tabella. Una classe del generatore non deve necessariamente implementare tutti i metodi e supportare tutte le conversioni presenti nella tabella precedente. I costrutti che non sono implementati non sono disponibili nelle espressioni di calcolo di tale tipo. Se, ad esempio, non si desidera supportare la parola chiave use nelle espressioni di calcolo, è possibile omettere la definizione di Use nella classe del generatore.

Nell'esempio di codice riportato di seguito viene illustrata un'espressione di calcolo che incapsula un calcolo come una serie di passaggi che può essere valutata una sola operazione alla volta. A discriminati tipo unione, OkOrException, codifica lo stato di errore dell'espressione come valutata finora. Questo codice dimostra diversi modelli tipici che è possibile utilizzare nelle espressioni di calcolo, ad esempio le implementazioni standard di alcuni dei metodi di creazione.

// Computations that can be run step by step
type Eventually<'T> =
    | Done of 'T
    | NotYetDone of (unit -> Eventually<'T>)

module Eventually =
    // The bind for the computations. Append 'func' to the
    // computation.
    let rec bind func expr =
        match expr with
        | Done value -> NotYetDone (fun () -> func value)
        | NotYetDone work -> NotYetDone (fun () -> bind func (work()))

    // Return the final value wrapped in the Eventually type.
    let result value = Done value

    type OkOrException<'T> =
    | Ok of 'T
    | Exception of System.Exception

    // 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 -> Exception exn
            match res with
            | Ok cont -> catch cont // note, a tailcall
            | Exception exn -> result (Exception 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 rest of the operations are boilerplate.
    // 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
                            | Exception 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 | Exception 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.
    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 "NotYetDone <closure>"
comp |> step |> step |> step |> step |> step |> step



// prints "x = 1"
// prints "x = 2"
// returns "Done 7"
comp |> step |> step |> step |> step |> step |> step |> step |> step

Un'espressione di calcolo è un tipo sottostante, che restituisce l'espressione. Il tipo sottostante può rappresentare un risultato calcolato o un calcolo ritardo che può essere eseguito o può fornire un modo per scorrere di qualche tipo di insieme. Nell'esempio precedente, il tipo sottostante è Eventually.Per un'espressione di sequenza, il tipo sottostante è IEnumerable. Per un'espressione di query, il tipo sottostante è IQueryable. Per un flusso di lavoro asincrona, il tipo sottostante è Async. Il Async oggetto rappresenta il lavoro da eseguire per calcolare il risultato. Ad esempio, chiamare Async.RunSynchronously per eseguire un calcolo e restituire il risultato.

Operazioni personalizzate

È possibile definire un'operazione personalizzata a un'espressione di calcolo e utilizzare un'operazione personalizzata di un 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 la resa e per i metodi nell'espressione di calcolo. Per definire un'operazione personalizzata, inserirla in una classe del generatore per l'espressione di calcolo e quindi applicare la CustomOperationAttribute. Questo attributo accetta una stringa come argomento, ovvero il nome da utilizzare in un'operazione personalizzata. Questo nome viene fornito nell'ambito all'inizio della parentesi graffa dell'espressione di calcolo. Di conseguenza, è consigliabile utilizzare gli identificatori che hanno lo stesso nome di un'operazione personalizzata in questo blocco. Ad esempio, evitare l'utilizzo di identificatori, ad esempio all o last nelle espressioni di query.

Vedere anche

Riferimenti

Flussi di lavoro asincroni (F#)

sequenze (F#)

le espressioni di Query (F#)

Altre risorse

Riferimenti per il linguaggio F#