Partager via


Expressions de calcul (F#)

En F#, les expressions de calcul fournissent une syntaxe pratique pour l'écriture de calculs qui peuvent être séquencés et combinés à l'aide de liaisons et de constructions de flux de contrôle. Elles peuvent être utilisées pour fournir une syntaxe pratique pour les monades, qui sont une fonctionnalité de programmation fonctionnelle pouvant être utilisée pour gérer des données, le contrôle et les effets secondaires dans les programmes fonctionnels.

Workflows intégrés

Les expressions de séquence sont un exemple d'expression de calcul, au même titre que les flux de travail asynchrones et les expressions de requête. Pour plus d'informations, consultez Séquences, Workflows asynchrones, et expressions de requête.

Certaines fonctionnalités sont communes aux expressions de séquence et aux flux de travail asynchrones, et illustrent la syntaxe de base d'une expression de calcul :

builder-name { expression }

La syntaxe précédente spécifie que l'expression donnée est une expression de calcul d'un type spécifié par builder-name. L'expression de calcul peut être un flux de travail intégré, tel que seq ou async, ou peut être quelque chose que vous définissez. builder-name est l'identificateur pour une instance d'un type spécial appelé type de générateur. Le type de générateur est un type de classe qui définit des méthodes spéciales qui régissent la façon dont les fragments de l'expression de calcul sont combinés (autrement dit, le code qui contrôle le mode d'exécution de l'expression). Une autre façon de décrire une classe de générateur consiste à dire qu'elle vous permet de personnaliser l'opération de nombreuses constructions F#, telles que les boucles et les liaisons.

Dans les expressions de calcul, deux formulaires sont disponibles pour quelques constructions de langage courantes. Vous pouvez appeler les constructions de type variant en utilisant un « ! » (bang) sur certains mots clés, tels que let!, do!, etc. Ces formulaires spéciaux forcent certaines fonctions définies dans la classe de générateur à remplacer le comportement intégré ordinaire de ces opérations. Ces formes ressemblent à la forme yield! du mot clé yield utilisé dans les expressions de séquence. Pour plus d'informations, consultez Séquences.

Création d'un type d'expression de calcul

Vous pouvez définir les caractéristiques de vos propres expressions de calcul en créant une classe de générateur et en définissant certaines méthodes spéciales sur la classe. La classe de générateur peut éventuellement définir les méthodes, comme indiqué dans le tableau suivant.

Le tableau suivant décrit les méthodes qui peuvent être utilisées dans une classe de générateur de workflow.

Méthode

La ou les signatures typiques

Description

Bind

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

Appelée pour let! et do! dans les expressions de calcul.

Delay

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

Encapsule une expression de calcul sous forme de fonction.

Return

'T -> M<'T>

Appelée pour return dans les expressions de calcul.

ReturnFrom

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

Appelée pour return! dans les expressions de calcul.

Run

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

M<'T> -> 'T

Exécute une expression de calcul.

Combine

M<'T> * M<'T> -> M<'T> ou

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

Appelée pour le séquencement dans les expressions de calcul.

For

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

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

Appelée pour les expressions for...do dans les expressions de calcul.

TryFinally

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

Appelée pour les expressions try...finally dans les expressions de calcul.

TryWith

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

Appelée pour les expressions try...with dans les expressions de calcul.

Using

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

Appelée pour les liaisons use dans les expressions de calcul.

While

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

Appelée pour les expressions while...do dans les expressions de calcul.

Yield

'T -> M<'T>

Appelée pour les expressions yield dans les expressions de calcul.

YieldFrom

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

Appelée pour les expressions yield! dans les expressions de calcul.

Zero

unit -> M<'T>

Appelée pour les branches else vides des expressions if...then dans les expressions de calcul.

Beaucoup de méthodes d'une classe de builder utilisent et retournent une construction M<'T>, qui doit en général être un type défini séparément qui caractérise le genre des calculs combinés, par exemple, Async<'T> pour les flux de travail asynchrones et Seq<'T> pour les flux de travail de séquence. Les signatures de ces méthodes leur permettent d'être combinées et imbriquées les unes dans les autres, afin que l'objet de flux de travail retourné d'une construction puisse être passé à la suivante. Lorsque le compilateur analyse une expression de calcul, il convertit l'expression en une série d'appels de fonction imbriqués à l'aide des méthodes du tableau précédent et du code de l'expression de calcul.

L'expression imbriquée a la forme suivante :

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

Dans le code ci-dessus, les appels à Run et Delay sont omis s'ils ne sont pas définis dans la classe de générateur d'expressions de calcul. Le corps de l'expression de calcul, désigné ici par {| cexpr |}, est traduit en appels concernant les méthodes de la classe de générateur par les traductions décrites dans le tableau suivant. L'expression de calcul {| cexpr |} est définie de manière récursive selon ces traductions où expr est une expression F# et cexpr est une expression de calcul.

Expression

Traduction

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

Dans le tableau précédent, other-expr décrit une expression qui n'est pas autrement répertoriée dans le tableau. Une classe de générateur n'a pas besoin d'implémenter toutes les méthodes et de prendre en charge toutes les traductions répertoriées dans le tableau précédent. Ces constructions qui ne sont pas implémentées ne sont pas disponibles dans les expressions de calcul de ce type. Par exemple, si vous ne souhaitez pas prendre en charge le mot clé use dans vos expressions de calcul, vous pouvez omettre la définition de Use dans votre classe de générateur.

L'exemple de code suivant affiche une expression de calcul qui encapsule un calcul comme une série d'étapes qui peuvent être évaluées une étape à la fois. Un type d'union discriminée, OkOrException, encode l'état d'erreur de l'expression comme évalué jusqu'à présent. Ce code illustre plusieurs modèles courants que vous pouvez utiliser dans les expressions de calcul, telles que les implémentations de zones fixes de certaines méthodes de générateur.

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

Une expression de calcul a un type sous-jacent, que l'expression retourne. Le type sous-jacent peut représenter un résultat calculé ou un calcul différé qui peuvent être exécutés, ou peuvent fournir un moyen d'itérer au sein d'un type de collection. Dans l'exemple précédent, le type sous-jacent était Eventually. Pour une expression de séquence, le type sous-jacent est IEnumerable. Pour une expression de requête, le type sous-jacent est IQueryable. Pour un flux de travail asychronous, le type sous-jacent est Async. L'objet Async représente le travail à effectuer pour calculer le résultat. Par exemple, vous appelez Async.RunSynchronously pour exécuter un calcul et retourner le résultat.

Opérations personnalisées

Vous pouvez définir une opération personnalisée sur une expression de calcul et utiliser une opération personnalisée comme opérateur dans une expression de calcul. Par exemple, vous pouvez inclure un opérateur de requête dans une expression de requête. Lorsque vous définissez une opération personnalisée, vous devez définir les méthodes Yield et For dans l'expression de calcul. Pour définir une opération personnalisée, mettez-là dans une classe de concepteur pour l'expression de calcul, puis appliquer CustomOperationAttribute. Cet attribut accepte une chaîne comme argument, qui est le nom à utiliser dans une opération personnalisée. Ce nom entre dans la portée au début de l'accolade ouvrante de l'expression de calcul. Par conséquent, vous ne devez pas utiliser des identificateurs qui ont le même nom qu'une opération personnalisée dans ce bloc. Par exemple, évitez d'utiliser des identificateurs tels que all ou last dans les expressions de requête.

Voir aussi

Référence

Workflows asynchrones (F#)

Séquences (F#)

Expressions de requête (F#)

Autres ressources

Référence du langage F#