Compartir a través de


Expresiones de cálculo (F#)

Las expresiones de cálculo en F# proporcionan una sintaxis adecuada para escribir cálculos que se pueden secuenciar y combinar mediante enlaces y construcciones de flujo de control. Se pueden utilizar para proporcionar una sintaxis apropiada para los monads, una característica de programación funcional que se puede utilizar para administrar datos, controles y efectos secundarios en programas funcionales.

Flujos de trabajo integrados

Las expresiones de secuencia son un ejemplo de una expresión de cálculo, como flujos de trabajo asincrónicos y expresiones de consulta. Para obtener más información, consulte secuencias de, Flujos de trabajo asincrónico, y Las expresiones de consulta.

Algunas características son comunes a las expresiones de secuencia y los flujos de trabajo asincrónicos, y muestran la sintaxis básica de una expresión de cálculo:

builder-name { expression }

En la sintaxis anterior, se especifica que esa expresión es una expresión de cálculo del tipo especificado por builder-name. La expresión de cálculo puede ser un flujo de trabajo integrado, como seq o async, o puede ser algo definido por el usuario. builder-name es el identificador de una instancia de un tipo especial denominado tipo de generador. El tipo de generador es un tipo de clase que define los métodos especiales que determinan la forma en que se combinan los fragmentos de la expresión de cálculo; es decir, el código que controla cómo se ejecuta la expresión. Para describir una clase de generador de otra forma, se puede decir que permite personalizar el funcionamiento de muchas construcciones de F#, como bucles y enlaces.

En las expresiones de cálculo, existen dos formas disponibles para algunas construcciones de lenguaje comunes. Las construcciones variantes se pueden invocar usando el sufijo ! (bang) con determinadas palabras clave, como let!, do!, etc. Estas formas especiales dan lugar a que algunas funciones definidas en la clase de generador reemplacen el comportamiento integrado ordinario de estas operaciones. Estas formas se parecen a la forma yield! de la palabra clave yield que se usa en expresiones de secuencia. Para obtener más información, vea Secuencias.

Crear un nuevo tipo de expresión de cálculo

Puede definir las características de sus propias expresiones de cálculo creando una clase de generador y definiendo en ella algunos métodos especiales. De manera opcional, la clase de generador puede definir los métodos que figuran en la siguiente tabla.

En la tabla siguiente se describen los métodos que se pueden utilizar en una clase de generador de flujo de trabajo.

Método

Signaturas típicas

Descripción

Bind

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

Se invoca para let! y do! en las expresiones de cálculo.

Delay

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

Encapsula una expresión de cálculo como una función.

Return

'T -> M<'T>

Se invoca para return en las expresiones de cálculo.

ReturnFrom

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

Se invoca para return! en las expresiones de cálculo.

Run

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

M<'T> -> 'T

Ejecuta una expresión de cálculo.

Combine

M<'T> * M<'T> -> M<'T> o

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

Se le llama para las secuencias en las expresiones de cálculo.

For

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

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

Se invoca para las expresiones for...do en las expresiones de cálculo.

TryFinally

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

Se invoca para las expresiones try...finally en las expresiones de cálculo.

TryWith

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

Se invoca para las expresiones try...with en las expresiones de cálculo.

Using

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

Se invoca para los enlaces use en las expresiones de cálculo.

While

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

Se invoca para las expresiones while...do en las expresiones de cálculo.

Yield

'T -> M<'T>

Se invoca para las expresiones yield en las expresiones de cálculo.

YieldFrom

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

Se invoca para las expresiones yield! en las expresiones de cálculo.

Zero

unit -> M<'T>

Se invoca para las bifurcaciones else vacías de las expresiones if...then en las expresiones de cálculo.

Muchos de los métodos en una clase de generador de usar y devolver un M<'T> construcción, que normalmente es un tipo definido por separado que caracteriza el tipo de cálculos que se combinan, por ejemplo, Async<'T> de flujos de trabajo asincrónicos y Seq<'T> de flujos de trabajo de secuencia. Las signaturas de estos métodos les permiten combinarse y anidarse unos con otros, de modo que el objeto de flujo de trabajo devuelto por una construcción se pueda pasar a la siguiente. Cuando el compilador analiza una expresión de cálculo, la convierte en una serie de llamadas a funciones anidadas utilizando para ello los métodos de la tabla anterior y el código de la expresión de cálculo.

La expresión anidada tiene el formato siguiente:

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

En el código anterior, las llamadas a Run y Delay se omiten si no se definen en la clase de generador de expresiones de cálculo. El cuerpo de la expresión de cálculo, denotado aquí como {| cexpr |}, se convierte en llamadas que involucran a los métodos de la clase de generadores mediante las conversiones que se describen en la tabla siguiente. La expresión de cálculo {| cexpr |} se define de forma recursiva con arreglo a estas conversiones, donde expr es una expresión de F# y cexpr es una expresión de cálculo.

Expresión

Conversión

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

En la tabla anterior, other-expr describe una expresión que, de otra forma, no aparecería en la tabla. Una clase de generadores no necesita implementar todos los métodos y admite todas las conversiones que se indican en la tabla anterior. Las construcciones que no se implementan no están disponibles en las expresiones de cálculo de ese tipo. Por ejemplo, si no desea admitir la palabra clave use en las expresiones de cálculo, puede reemplazar la definición de Use en la clase de generadores.

En el ejemplo de código siguiente se muestra una expresión de cálculo que encapsula un cálculo como una serie de pasos que se puede evaluar un paso a la vez. A discrimina el tipo de unión, OkOrException, codifica el estado de error de la expresión que evalúa hasta el momento. Este código muestra varios patrones típicos que pueden utilizar en sus expresiones de cálculo, como, por ejemplo, las implementaciones de repetitivo de algunos de los métodos de generador.

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

Una expresión de cálculo tiene un tipo subyacente, que devuelve la expresión. El tipo subyacente puede representar un resultado calculado o un cálculo de demorado que se puede realizar, o puede proporcionar una forma de iterar a través de algún tipo de colección. En el ejemplo anterior, el tipo subyacente era Eventually.Una expresión de secuencia, el tipo subyacente es IEnumerable. Una expresión de consulta, el tipo subyacente es IQueryable. Para un flujo de trabajo asincrónico, el tipo subyacente es Async. El Async objeto representa el trabajo que debe realizar para calcular el resultado. Por ejemplo, se llama a Async.RunSynchronously para ejecutar un cálculo y devolver el resultado.

Operaciones personalizadas

Puede definir una operación personalizada en una expresión de cálculo y utilizar una operación personalizada como un operador en una expresión de cálculo. Por ejemplo, puede incluir un operador de consulta en una expresión de consulta. Al definir una operación personalizada, debe definir el rendimiento y los métodos en la expresión de cálculo. Para definir una operación personalizada, poner en una clase de generador para la expresión de cálculo y, a continuación, aplique el CustomOperationAttribute. Este atributo toma una cadena como argumento, que es el nombre que se utilizará en una operación personalizada. Este nombre entra en el ámbito de aplicación del principio de la llave de apertura de la expresión de cálculo. Por lo tanto, no debe utilizar identificadores que tienen el mismo nombre que una operación personalizada en este bloque. Por ejemplo, evite el uso de identificadores, como all o last en las expresiones de consulta.

Vea también

Referencia

Los flujos de trabajo asincrónicos (F#)

secuencias (F#)

expresiones de consulta (F#)

Otros recursos

Referencia del lenguaje F#