Expresiones de cálculo

Las expresiones de cálculo de F# proporcionan una sintaxis práctica para escribir cálculos que se pueden secuenciar y combinar mediante construcciones y enlaces de flujo de control. Dependiendo del tipo de expresión de cálculo, se pueden pensar en ellos como una manera de expresar las canaladas, los monoids, los transformadores de cadena y los functors aplicativos. Sin embargo, a diferencia de otros lenguajes (como la notación do en Hasvención), no están vinculados a una sola abstracción y no se basan en macros u otras formas de metaprogramación para lograr una sintaxis cómoda y contextual.

Información general

Los cálculos pueden tener muchas formas. La forma más común de cálculo es la ejecución de un solo subproceso, que es fácil de entender y modificar. Sin embargo, no todas las formas de cálculo son tan sencillas como la ejecución de un solo subproceso. Estos son algunos ejemplos:

  • Cálculos no deterministas
  • Cálculos asincrónicos
  • Cálculos con efecto
  • Cálculos generativos

En general, hay cálculos contextuales que debe realizar en determinadas partes de una aplicación. Escribir código contextual puede ser complicado, ya que es fácil "filtrar" cálculos fuera de un contexto determinado sin abstracciones para evitar que lo haga. Estas abstracciones suelen ser difíciles de escribir por sí solas, por lo que F# tiene una manera generalizada de hacerlo denominadas expresiones de cálculo.

Las expresiones de cálculo ofrecen un modelo uniforme de sintaxis y abstracción para codificar cálculos contextuales.

Cada expresión de cálculo está copiada por un tipo de generador. El tipo de generador define las operaciones que están disponibles para la expresión de cálculo. Vea Crear un nuevo tipo de expresión de cálculo, que muestra cómo crear una expresión de cálculo personalizada.

Información general sobre la sintaxis

Todas las expresiones de cálculo tienen el formato siguiente:

builder-expr { cexper }

En este formato, es el nombre de un tipo de generador que define la expresión de cálculo y es el cuerpo de expresión builder-expr de la expresión de cexper cálculo. Por ejemplo, async el código de expresión de cálculo puede tener este aspecto:

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

        let processedData = processData data

        return processedData
    }

Hay una sintaxis especial y adicional disponible dentro de una expresión de cálculo, como se muestra en el ejemplo anterior. Las siguientes formas de expresión son posibles con expresiones de cálculo:

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

Cada una de estas palabras clave y otras palabras clave estándar de F# solo están disponibles en una expresión de cálculo si se han definido en el tipo de generador de respaldo. La única excepción a esto es , que es en sí mismo el nivel sintáctico para el uso de seguido de una coincidencia match! de patrón en el let! resultado.

El tipo de generador es un objeto que define métodos especiales que rigen la forma en que se combinan los fragmentos de la expresión de cálculo; es decir, sus métodos controlan cómo se comporta la expresión de cálculo. Otra manera de describir una clase de generador es decir que permite personalizar el funcionamiento de muchas construcciones de F#, como bucles y enlaces.

let!

La let! palabra clave enlaza el resultado de una llamada a otra expresión de cálculo a un nombre:

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

Si enlaza la llamada a una expresión de cálculo con , no se let obtiene el resultado de la expresión de cálculo. En su lugar, habrá enlazado el valor de la llamada no realizada a esa expresión de cálculo. Use let! para enlazar al resultado.

let! lo define el miembro Bind(x, f) en el tipo de generador.

do!

La palabra clave es para llamar a una expresión de cálculo que devuelve un do! tipo unit -like (definido por el miembro en el Zero generador):

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

Para el flujo de trabajo asincrónico, este tipo es Async<unit> . Para otras expresiones de cálculo, es probable que el tipo sea CExpType<unit> .

do! lo define el miembro Bind(x, f) en el tipo de generador, donde genera un f unit .

yield

La yield palabra clave es para devolver un valor de la expresión de cálculo para que se pueda consumir como IEnumerable<T> :

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

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

En la mayoría de los casos, los autores de la llamada pueden omitirlo. La manera más común de omitir yield es con el operador -> :

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

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

Para expresiones más complejas que podrían producir muchos valores diferentes y quizás condicionalmente, simplemente omitir la palabra clave puede hacer lo siguiente:

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

Al igual que con la palabra clave yield en C#,cada elemento de la expresión de cálculo se produce a medida que se itera.

yield lo define el miembro Yield(x) en el tipo de generador, donde es el elemento que se va a x devolver.

yield!

La yield! palabra clave es para aplanar una colección de valores de una expresión de cálculo:

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

Cuando se evalúa, la expresión de cálculo a la que llama hará que sus elementos se resalten uno a uno, lo que yield! aplana el resultado.

yield! lo define el miembro YieldFrom(x) en el tipo de generador, donde es una colección de x valores.

A yield diferencia de , debe yield! especificarse explícitamente. Su comportamiento no es implícito en las expresiones de cálculo.

return

La return palabra clave encapsula un valor en el tipo correspondiente a la expresión de cálculo. Además de las expresiones de cálculo que usan yield , se usa para "completar" una expresión de cálculo:

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 lo define el miembro Return(x) en el tipo de generador, donde es el elemento que se va a x ajustar.

return!

La palabra clave se da cuenta del valor de una expresión de cálculo y encapsula el tipo return! correspondiente a la expresión de cálculo:

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

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

return! lo define el miembro ReturnFrom(x) en el tipo de generador, donde es otra expresión de x cálculo.

match!

La palabra clave permite en línea una llamada a otra expresión de cálculo y coincidencia de patrones match! en su resultado:

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

Al llamar a una expresión de cálculo con match! , se dará cuenta del resultado de la llamada como let! . Esto se usa a menudo al llamar a una expresión de cálculo donde el resultado es opcional.

Expresiones de cálculo integradas

La biblioteca principal de F# define cuatro expresionesde cálculo integradas: expresiones de secuencia, expresiones asincrónicas, expresiones de tareay expresiones de consulta.

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 determinados métodos especiales en la clase . La clase de generador puede definir opcionalmente los métodos como se muestra en la tabla siguiente.

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

Método Firmas típicas Descripción
Bind M<'T> * ('T -> M<'U>) -> M<'U> Se llama para let! y do! en las expresiones de cálculo.
Delay (unit -> M<'T>) -> Delayed<'T> Encapsula una expresión de cálculo como una función. Delayed<'T> puede ser cualquier tipo, normalmente o M<'T> unit -> M<'T> se usan. La implementación predeterminada devuelve M<'T> un .
Return 'T -> M<'T> Se llama a return en las expresiones de cálculo.
ReturnFrom M<'T> -> M<'T> Se llama a return! en las expresiones de cálculo.
Run Delayed<'T> -> M<'T> o

M<'T> -> 'T
Ejecuta una expresión de cálculo.
Combine M<'T> * Delayed<'T> -> M<'T> o

M<unit> * M<'T> -> M<'T>
Se llama para la secuenciación en expresiones de cálculo.
For seq<'T> * ('T -> M<'U>) -> M<'U> o

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Se llama for...do para expresiones en expresiones de cálculo.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Se llama try...finally para expresiones en expresiones de cálculo.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Se llama try...with para expresiones en expresiones de cálculo.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Se llama use para los enlaces en expresiones de cálculo.
While (unit -> bool) * Delayed<'T> -> M<'T>or

(unit -> bool) * Delayed<unit> -> M<unit>
Se llama while...do para expresiones en expresiones de cálculo.
Yield 'T -> M<'T> Se llama yield para expresiones en expresiones de cálculo.
YieldFrom M<'T> -> M<'T> Se llama yield! para expresiones en expresiones de cálculo.
Zero unit -> M<'T> Se llama para else ramas if...then vacías de expresiones en expresiones de cálculo.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Indica que la expresión de cálculo se pasa al Run miembro como comilla. Convierte todas las instancias de un cálculo en comillas.

Muchos de los métodos de una clase de generador usan y devuelven una construcción, que suele ser un tipo definido por separado que caracteriza el tipo de cálculos que se combinan, por ejemplo, para expresiones asincrónicas y flujos de trabajo de M<'T> Async<'T> Seq<'T> secuencia. Las firmas de estos métodos permiten combinarlos y anidarlos entre sí, de modo que el objeto de flujo de trabajo devuelto de una construcción se pueda pasar a la siguiente.

Muchas funciones usan el resultado de Delay como argumento: Run , , , y While TryWith TryFinally Combine . El Delayed<'T> tipo es el tipo de valor devuelto de Delay y, por consiguiente, el parámetro para estas funciones. Delayed<'T> puede ser un tipo arbitrario que no necesita estar relacionado con M<'T> ; normalmente o se M<'T> (unit -> M<'T>) usan. La implementación predeterminada es M<'T> . Consulte aquí para obtener una apariencia más detallada.

El compilador, cuando analiza una expresión de cálculo, convierte la expresión en una serie de llamadas a funciones anidadas mediante 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 y se omiten si no están definidas en la clase generadora de Run Delay expresiones de cálculo. El cuerpo de la expresión de cálculo, aquí indicado como , se traduce en llamadas que implican los métodos de la clase de generador mediante las traducciones descritas en {| cexpr |} la tabla siguiente. La expresión de cálculo se define de forma recursiva según estas traducciones, donde es una expresión de F# y {| cexpr |} es una expresión de expr cexpr cálculo.

Expression Traducció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(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 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 -> 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()

En la tabla anterior, describe una expresión que no aparece en la other-expr tabla. Una clase de generador no necesita implementar todos los métodos y admitir todas las traducciones enumeradas en la tabla anterior. Las construcciones que no se implementan no están disponibles en expresiones de cálculo de ese tipo. Por ejemplo, si no desea admitir la palabra clave en las expresiones de cálculo, puede omitir la definición use de en la clase de Use generador.

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 pueden evaluar paso a paso. Un tipo de unión discriminada, OkOrException , codifica el estado de error de la expresión como se ha evaluado hasta ahora. Este código muestra varios patrones típicos que puede usar en las expresiones de cálculo, como implementaciones reutilizables de algunos de los métodos de generador.

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

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 retrasado que se puede realizar, o puede proporcionar una manera de recorrer en iteración algún tipo de colección. En el ejemplo anterior, el tipo subyacente era Eventually<_> . Para una expresión de secuencia, el tipo subyacente es System.Collections.Generic.IEnumerable<T> . Para una expresión de consulta, el tipo subyacente es System.Linq.IQueryable . Para una expresión asincrónica, el tipo subyacente es Async . El Async objeto representa el trabajo que se va a realizar para calcular el resultado. Por ejemplo, se llama Async.RunSynchronously a 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 usar una operación personalizada como 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 los métodos Yield y For en la expresión de cálculo. Para definir una operación personalizada, pónla en una clase de generador para la expresión de cálculo y, a continuación, aplique CustomOperationAttribute . Este atributo toma una cadena como argumento, que es el nombre que se va a usar en una operación personalizada. Este nombre entra en el ámbito al principio de la llave de apertura de la expresión de cálculo. Por lo tanto, no debe usar identificadores que tengan el mismo nombre que una operación personalizada en este bloque. Por ejemplo, evite el uso de identificadores como all o last en expresiones de consulta.

Extensión de los generadores existentes con nuevas operaciones personalizadas

Si ya tiene una clase de generador, sus operaciones personalizadas se pueden extender desde fuera de esta clase de generador. Las extensiones deben declararse en módulos. Los espacios de nombres no pueden contener miembros de extensión excepto en el mismo archivo y el mismo grupo de declaración de espacio de nombres donde se define el tipo.

En el ejemplo siguiente se muestra la extensión de la clase FSharp.Linq.QueryBuilder existente.

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

Las operaciones personalizadas se pueden sobrecargar. Para obtener más información, vea F# RFC FS-1056 - Allow overloads of custom keywords in computation expressions (F# RFC FS-1056:permitir sobrecargas de palabras clave personalizadas en expresiones de cálculo).

Compilación eficaz de expresiones de cálculo

Las expresiones de cálculo de F# que suspenden la ejecución se pueden compilar en máquinas de estado altamente eficientes mediante un uso cuidadoso de una característica de bajo nivel denominada código reanudable. El código reanudable se documenta en F# RFC FS-1087 y se usa para las expresiones de tarea.

Las expresiones de cálculo de F# que son sincrónicas (es decir, no suspenden la ejecución) también se pueden compilar en máquinas de estado eficientes mediante funciones insertadas, incluido el InlineIfLambda atributo . Se proporcionan ejemplos en F# RFC FS-1098.

El compilador de F# ofrece un tratamiento especial a las expresiones de lista, las expresiones de matriz y las expresiones de secuencia para garantizar la generación de código de alto rendimiento.

Vea también