Partilhar via


Expressões computacionais

As expressões de computação em F# fornecem uma sintaxe conveniente para escrever cálculos que podem ser sequenciados e combinados usando construções e ligações de fluxo de controle. Dependendo do tipo de expressão computacional, eles podem ser pensados como uma forma de expressar mônadas, monoides, transformadores de mônadas e functores aplicadores. No entanto, ao contrário de outras linguagens (como a notação em Haskell), elas não estão ligadas a uma única abstração e não dependem de macros ou outras formas de metaprogramação para realizar uma sintaxe conveniente e sensível ao contexto.

Descrição geral

Os cálculos podem assumir muitas formas. A forma mais comum de computação é a execução single-threaded, que é fácil de entender e modificar. No entanto, nem todas as formas de computação são tão simples quanto a execução single-threaded. Alguns exemplos incluem:

  • Cálculos não determinísticos
  • Cálculos assíncronos
  • Cálculos efetivos
  • Cálculos generativos

Mais geralmente, há cálculos sensíveis ao contexto que você deve executar em determinadas partes de um aplicativo. Escrever código sensível ao contexto pode ser um desafio, pois é fácil "vazar" cálculos fora de um determinado contexto sem abstrações para impedir que você faça isso. Essas abstrações são muitas vezes desafiadoras para escrever por si mesmo, e é por isso que o F# tem uma maneira generalizada de fazer as chamadas expressões computacionais.

As expressões computacionais oferecem uma sintaxe uniforme e um modelo de abstração para codificar cálculos sensíveis ao contexto.

Cada expressão de computação é apoiada por um tipo de construtor . O tipo builder define as operações que estão disponíveis para a expressão de computação. Consulte Criando um novo tipo de expressão de computação, que mostra como criar uma expressão de computação personalizada.

Visão geral da sintaxe

Todas as expressões computacionais têm a seguinte forma:

builder-expr { cexper }

Nesta forma, builder-expr é o nome de um tipo de construtor que define a expressão de computação e cexper é o corpo de expressão da expressão de computação. Por exemplo, async o código de expressão computacional pode ter esta aparência:

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

        let processedData = processData data

        return processedData
    }

Há uma sintaxe especial e adicional disponível dentro de uma expressão de computação, como mostrado no exemplo anterior. As seguintes formas de expressão são possíveis com expressões computacionais:

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

Cada uma dessas palavras-chave e outras palavras-chave F# padrão só estão disponíveis em uma expressão de computação se tiverem sido definidas no tipo de construtor de suporte. A única exceção é match!o , que é em si açúcar sintático para o uso seguido de let! uma correspondência de padrão no resultado.

O tipo construtor é um objeto que define métodos especiais que governam a maneira como os fragmentos da expressão computacional são combinados; ou seja, seus métodos controlam como a expressão computacional se comporta. Outra maneira de descrever uma classe builder é dizer que ela permite que você personalize a operação de muitas construções F#, como loops e associações.

let!

A let! palavra-chave associa o resultado de uma chamada a outra expressão de computação a um nome:

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

Se você vincular a chamada a uma expressão de computação com let, não obterá o resultado da expressão de computação. Em vez disso, você terá vinculado o valor da chamada não realizada a essa expressão de computação. Use let! para vincular ao resultado.

let! é definido pelo Bind(x, f) membro no tipo de construtor.

and!

A and! palavra-chave permite vincular os resultados de várias chamadas de expressão computacional de maneira eficiente.

let doThingsAsync url =
    async {
        let! data = getDataAsync url
        and! moreData = getMoreDataAsync anotherUrl
        and! evenMoreData = getEvenMoreDataAsync someUrl
        ...
    }

Usando uma série de let! ... let! ... forças de re-execução de ligações caras, assim usar let! ... and! ... deve ser usado ao ligar os resultados de numerosas expressões de computação.

and! é definido principalmente pelo MergeSources(x1, x2) membro no tipo de construtor.

Opcionalmente, MergeSourcesN(x1, x2 ..., xN) pode ser definido para reduzir o número de nós de tupling e BindN(x1, x2 ..., xN, f), ou BindNReturn(x1, x2, ..., xN, f) pode ser definido para vincular resultados de expressão de computação de forma eficiente sem tupling nós.

do!

A do! palavra-chave é para chamar uma expressão de computação que retorna um unittipo -like (definido pelo Zero membro no construtor):

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

Para o fluxo de trabalho assíncrono, esse tipo é Async<unit>. Para outras expressões computacionais, é provável que o tipo seja CExpType<unit>.

do! é definido pelo Bind(x, f) membro no tipo construtor, onde f produz um unitarquivo .

yield

A yield palavra-chave é para retornar um valor da expressão de computação para que ele possa ser consumido como um IEnumerable<T>:

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

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

Na maioria dos casos, ele pode ser omitido pelos chamadores. A maneira mais comum de omitir yield é com o -> operador:

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

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

Para expressões mais complexas que podem gerar muitos valores diferentes, e talvez condicionalmente, simplesmente omitir a palavra-chave pode fazer:

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

Tal como acontece com a palavra-chave yield em C#, cada elemento na expressão de computação é reproduzido à medida que é iterado.

yield é definido pelo Yield(x) membro no tipo construtor, onde x é o item a ser devolvido.

yield!

A yield! palavra-chave é para nivelar uma coleção de valores de uma expressão de computação:

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 avaliada, a expressão computacional chamada por yield! terá seus itens reproduzidos um a um, achatando o resultado.

yield! é definido pelo YieldFrom(x) membro no tipo construtor, onde x é uma coleção de valores.

Ao contrário yieldde , yield! deve ser explicitamente especificado. Seu comportamento não está implícito em expressões computacionais.

return

A return palavra-chave encapsula um valor no tipo correspondente à expressão de computação. Além de expressões computacionais usando yield, ele é usado para "completar" uma expressão de computação:

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 é definido pelo Return(x) membro no tipo construtor, onde x é o item a ser encapsulado. Para let! ... return uso, BindReturn(x, f) pode ser usado para melhorar o desempenho.

return!

A return! palavra-chave percebe o valor de uma expressão de computação e encapsula que resultam no tipo correspondente à expressão de computação:

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

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

return! é definido pelo ReturnFrom(x) membro no tipo construtor, onde x é outra expressão de computação.

match!

A match! palavra-chave permite que você embuta uma chamada para outra expressão de computação e correspondência de padrão em seu resultado:

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

Ao chamar uma expressão de computação com match!, ele perceberá o resultado da chamada como let!. Isso é frequentemente usado ao chamar uma expressão de computação onde o resultado é opcional.

Expressões computacionais incorporadas

A biblioteca principal do F# define quatro expressões de computação internas: Expressões de Sequência, Expressões Assíncronas, Expressões de Tarefa e Expressões de Consulta.

Criando um novo tipo de expressão computacional

Você pode definir as características de suas próprias expressões de computação criando uma classe builder e definindo certos métodos especiais na classe. A classe builder pode, opcionalmente, definir os métodos conforme listados na tabela a seguir.

A tabela a seguir descreve métodos que podem ser usados em uma classe de construtor de fluxo de trabalho.

Método Assinatura(s) típica(s) Descrição
Bind M<'T> * ('T -> M<'U>) -> M<'U> Chamado e let! do! em expressões computacionais.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Exigiu expressões eficientes let! e and! computacionais sem mesclar entradas.

por exemplo Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Envolve uma expressão de computação como uma função. Delayed<'T> podem ser de qualquer tipo, comumente M<'T> ou unit -> M<'T> são usados. A implementação padrão retorna um M<'T>arquivo .
Return 'T -> M<'T> Chamado em return expressões computacionais.
ReturnFrom M<'T> -> M<'T> Chamado em return! expressões computacionais.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Apelou a uma eficiência let! ... return nas expressões computacionais.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Exigiu eficiência let! ... and! ... return em expressões computacionais sem mesclar entradas.

por exemplo Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> Chamado em and! expressões computacionais.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Solicitado and! em expressões computacionais, mas melhora a eficiência reduzindo o número de nós tupling.

por exemplo MergeSources3, MergeSources4.
Run Delayed<'T> -> M<'T> ou

M<'T> -> 'T
Executa uma expressão de computação.
Combine M<'T> * Delayed<'T> -> M<'T> ou

M<unit> * M<'T> -> M<'T>
Chamado para sequenciamento em expressões computacionais.
For seq<'T> * ('T -> M<'U>) -> M<'U> ou

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Chamado para for...do expressões em expressões computacionais.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Chamado para try...finally expressões em expressões computacionais.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Chamado para try...with expressões em expressões computacionais.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Chamado para use ligações em expressões computacionais.
While (unit -> bool) * Delayed<'T> -> M<'T>quer

(unit -> bool) * Delayed<unit> -> M<unit>
Chamado para while...do expressões em expressões computacionais.
Yield 'T -> M<'T> Chamado para yield expressões em expressões computacionais.
YieldFrom M<'T> -> M<'T> Chamado para yield! expressões em expressões computacionais.
Zero unit -> M<'T> Chamado para ramos vazios else de if...then expressões em expressões computacionais.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Indica que a expressão de computação é passada para o Run membro como uma citação. Ele traduz todas as instâncias de um cálculo em uma cotação.

Muitos dos métodos em uma classe builder usam e retornam uma M<'T> construção, que normalmente é um tipo definido separadamente que caracteriza o tipo de cálculos que estão sendo combinados, por exemplo, Async<'T> para expressões assíncronas e Seq<'T> para fluxos de trabalho de sequência. As assinaturas desses métodos permitem que eles sejam combinados e aninhados uns com os outros, para que o objeto de fluxo de trabalho retornado de uma construção possa ser passado para a próxima.

Muitas funções usam o resultado de Delay como argumento: Run, While, TryWith, TryFinally, e Combine. O Delayed<'T> tipo é o tipo de retorno e Delay , consequentemente, o parâmetro para essas funções. Delayed<'T> pode ser um tipo arbitrário que não precisa ser relacionado com M<'T>; comumente M<'T> ou (unit -> M<'T>) são usados. A implementação padrão é M<'T>. Veja aqui um olhar mais aprofundado.

O compilador, quando analisa uma expressão de computação, traduz a expressão em uma série de chamadas de função aninhadas usando os métodos na tabela anterior e o código na expressão de computação. A expressão aninhada tem a seguinte forma:

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

No código acima, as chamadas para Run e Delay são omitidas se não estiverem definidas na classe computation expression builder. O corpo da expressão computacional, aqui denotado como {{ cexpr }}, é traduzido em outras chamadas para os métodos da classe builder. Este processo é definido recursivamente de acordo com as traduções na tabela a seguir. O código entre parênteses duplos {{ ... }} ainda precisa ser traduzido, expr representa uma expressão F# e cexpr representa uma expressão computacional.

Expression Tradução
{{ 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()

Na tabela anterior, other-expr descreve uma expressão que não está listada na tabela. Uma classe builder não precisa implementar todos os métodos e dar suporte a todas as traduções listadas na tabela anterior. As construções que não são implementadas não estão disponíveis em expressões computacionais desse tipo. Por exemplo, se você não quiser oferecer suporte use à palavra-chave em suas expressões de computação, poderá omitir a definição de em sua classe builder Use .

O exemplo de código a seguir mostra uma expressão de computação que encapsula uma computação como uma série de etapas que podem ser avaliadas uma etapa de cada vez. Um tipo de união discriminada, OkOrException, codifica o estado de erro da expressão conforme avaliado até agora. Esse código demonstra vários padrões típicos que você pode usar em suas expressões de computação, como implementações clichês de alguns dos métodos do construtor.

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

Uma expressão de computação tem um tipo subjacente, que a expressão retorna. O tipo subjacente pode representar um resultado calculado ou um cálculo atrasado que pode ser executado, ou pode fornecer uma maneira de iterar através de algum tipo de coleção. No exemplo anterior, o tipo subjacente era Eventually<_>. Para uma expressão de sequência, o tipo subjacente é System.Collections.Generic.IEnumerable<T>. Para uma expressão de consulta, o tipo subjacente é System.Linq.IQueryable. Para uma expressão assíncrona, o tipo subjacente é Async. O Async objeto representa o trabalho a ser executado para calcular o resultado. Por exemplo, você chama Async.RunSynchronously para executar um cálculo e retornar o resultado.

Operações personalizadas

Você pode definir uma operação personalizada em uma expressão de computação e usar uma operação personalizada como um operador em uma expressão de computação. Por exemplo, você pode incluir um operador de consulta em uma expressão de consulta. Ao definir uma operação personalizada, você deve definir os métodos Yield e For na expressão de computação. Para definir uma operação personalizada, coloque-a em uma classe builder para a expressão de computação e, em seguida, aplique o CustomOperationAttribute. Esse atributo usa uma cadeia de caracteres como um argumento, que é o nome a ser usado em uma operação personalizada. Esse nome entra em escopo no início da chave de abertura da expressão computacional. Portanto, você não deve usar identificadores que tenham o mesmo nome de uma operação personalizada neste bloco. Por exemplo, evite o uso de identificadores como all ou last em expressões de consulta.

Ampliando construtores existentes com novas operações personalizadas

Se você já tiver uma classe builder, suas operações personalizadas podem ser estendidas de fora dessa classe builder. As extensões devem ser declaradas em módulos. Os namespaces não podem conter membros de extensão, exceto no mesmo arquivo e no mesmo grupo de declaração de namespace onde o tipo é definido.

O exemplo a seguir mostra a extensão da classe existente FSharp.Linq.QueryBuilder .

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

As operações personalizadas podem ser sobrecarregadas. Para obter mais informações, consulte F# RFC FS-1056 - Permitir sobrecargas de palavras-chave personalizadas em expressões de computação.

Compilando expressões computacionais de forma eficiente

As expressões de computação F# que suspendem a execução podem ser compiladas em máquinas de estado altamente eficientes através do uso cuidadoso de um recurso de baixo nível chamado código retomável. O código retomável é documentado em F# RFC FS-1087 e usado para expressões de tarefas.

As expressões de computação F# que são síncronas (ou seja, não suspendem a execução) podem, alternativamente, ser compiladas em máquinas de estado eficientes usando funções embutidas, incluindo o InlineIfLambda atributo. Exemplos são dados em F# RFC FS-1098.

Expressões de lista, expressões de matriz e expressões de sequência recebem tratamento especial do compilador F# para garantir a geração de código de alto desempenho.

Consulte também