Compartilhar via


Expressões de computação

As expressões de computação em F# fornecem uma sintaxe conveniente para escrever cálculos que podem ser sequenciados e combinados usando constructos e associações de fluxo de controle. Dependendo do tipo de expressão de computação, elas podem ser consideradas como uma maneira de expressar monads, monoides, transformadores de monad e funtores applicativos. No entanto, ao contrário de outras linguagens (como do-notation em Haskell), elas não estão vinculadas a uma só abstração e não dependem de macros ou outras formas de metaprogramação para realizar uma sintaxe conveniente e sensível ao contexto.

Visão geral

Os cálculos podem assumir várias formas. A forma mais comum de computação é a execução de thread único, 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 em um único thread. Alguns exemplos incluem:

  • Cálculos não determinísticos
  • Cálculos assíncronos
  • Computações com efeito
  • Cálculos generativos

Em geral, há computações sensíveis ao contexto que você deve executar em determinadas partes de um aplicativo. Escrever código sensível ao contexto pode ser desafiador, pois é fácil "vazar" processos fora de um determinado contexto sem abstrações que o impeçam. Essas abstrações geralmente são desafiadoras para escrever por conta própria, e é por isso que o F# tem uma maneira generalizada de fazê-lo chamada expressões de cálculo .

As expressões de computação oferecem um modelo uniforme de sintaxe e abstração para codificar cálculos contextuais.

Cada expressão de computação é suportada por um construtor de tipo. O tipo de construtor define as operações 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 de computação têm o seguinte formulário:

builder-expr { cexper }

Nesse formulário, builder-expr é o nome de um tipo de construtor que define a expressão de computação e cexper é o corpo da expressão de computação. Por exemplo, async código de expressão de computação 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 em uma expressão de computação, conforme mostrado no exemplo anterior. Os formulários de expressão a seguir são possíveis com expressões de computação:

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 padrão do F# só estarão disponíveis em uma expressão de computação se tiverem sido definidas no tipo de construtor de apoio. A única exceção para isso é match!, que é, em si, açúcar sintático para uso de let! seguido uma correspondência de padrão no resultado.

O tipo de construtor é um objeto que define métodos especiais que regem a maneira como os fragmentos da expressão de computação são combinados; ou seja, seus métodos controlam como a expressão de computação se comporta. Outra maneira de descrever uma classe de construtor é dizer que ela permite personalizar a operação de muitos constructos F#, como loops e associações.

let!

A palavra-chave let! 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ê associar 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á associado o valor da chamada não realizada a essa expressão de computação. Use let! para associar ao resultado.

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

and!

A palavra-chave and! permite associar os resultados de várias chamadas de expressão de computação de maneira performativa.

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

Usar uma série de let! ... let! ... força a reexecução de associações caras, portanto, deve-se usar let! ... and! ... ao associar os resultados de várias expressões de computação.

and! é definido principalmente pelo membro MergeSources(x1, x2) 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) podem ser definidos para vincular de forma eficiente os resultados das expressões de computação sem usar nós de tupling.

do!

A palavra-chave do! é para chamar uma expressão de computação que retorna um tipo semelhante a unit(definido pelo membro Zero 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 de computação, é provável que o tipo seja CExpType<unit>.

do! é definido pelo membro Bind(x, f) no tipo de construtor, em que f produz um unit.

yield

A palavra-chave yield é para retornar um valor da expressão de computação para que possa ser consumida 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 usuários. 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 produzir 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"
    }

Assim como acontece com a palavra-chave de geração em C#, cada elemento na expressão de computação é gerado de volta à medida que é iterado.

yield é definido pelo membro Yield(x) no tipo construtor, em que x é o item a ser retornado.

yield!

A palavra-chave yield! é 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 de computação chamada por yield! terá seus itens gerados um por um, nivelando o resultado.

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

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

return

A palavra-chave return encapsula um valor no tipo correspondente à expressão de computação. Além das expressões de computação que usam yield, ela é usada para "concluir" 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 membro Return(x) no tipo de construtor, em que x está o item a ser encapsulado. No uso de let! ... return, BindReturn(x, f) pode ser utilizado para melhorar o desempenho.

return!

A palavra-chave return! realiza o valor de uma expressão de computação e encapsula esse resultado 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 membro ReturnFrom(x) no tipo construtor, em que x é outra expressão de computação.

match!

A palavra-chave match! permite embutir uma chamada em outra expressão de computação e faça a 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 cálculo com match!, ela executará o resultado da chamada como let!. Isso geralmente é usado ao chamar uma expressão de computação em que o resultado é opcional.

Expressões de computação internas

A biblioteca principal F# define quatro expressões de computação internas: Sequence Expressions, expressões assíncronas, expressões Taske expressões de consulta .

Criando um novo tipo de expressão de computação

Você pode definir as características de suas próprias expressões de computação criando uma classe de construtor e definindo determinados métodos especiais na classe. Opcionalmente, a classe de construtor pode definir os métodos conforme listado 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 Assinaturas típicas Descrição
Bind M<'T> * ('T -> M<'U>) -> M<'U> Chamado para let! e do! em expressões de computação.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Chamado para let! e and! eficientes em expressões de computação sem mesclar entradas.

por exemplo, Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Encapsula uma expressão de computação como uma função. Delayed<'T> pode ser qualquer tipo, geralmente M<'T> ou unit -> M<'T> são usados. A implementação padrão retorna um M<'T>.
Return 'T -> M<'T> Chamado para return em expressões de computação.
ReturnFrom M<'T> -> M<'T> Chamado para return! em expressões de computação.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Chamado para um let! ... return eficiente em expressões de computação.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Chamado para let! ... and! ... return eficiente em expressões de computação sem mesclar entradas.

por exemplo, Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> Chamado para and! em expressões de computação.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> Chamado para and! em expressões de computação, mas melhora a eficiência reduzindo o número de nós de 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 de computação.
For seq<'T> * ('T -> M<'U>) -> M<'U> ou

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

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

Muitos dos métodos em uma classe de construtor usam e retornam um constructo M<'T>, 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 entre si, de modo que o objeto de fluxo de trabalho retornado de uma construção possa ser passado para o próximo.

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

O compilador, quando analisa uma expressão de computação, converte 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 é da seguinte forma:

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

No código acima, as chamadas para Run e Delay serão omitidas se não estiverem definidas na classe construtor de expressões de computação. O corpo da expressão de computação, aqui indicado como {{ cexpr }}, é convertido em outras chamadas para os métodos da classe de construtor. Esse processo é definido recursivamente de acordo com as traduções na tabela a seguir. O código dentro de colchetes duplos {{ ... }} continua a ser traduzido, expr representa uma expressão F# e cexpr representa uma expressão de computação.

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 de outra forma na tabela. Uma classe de construtor não precisa implementar todos os métodos e dar suporte a todas as traduções listadas na tabela anterior. Esses constructos que não são implementados não estão disponíveis em expressões de computação desse tipo. Por exemplo, se você não quiser dar suporte à palavra-chave use em suas expressões de computação, poderá omitir a definição de Use em sua classe de construtor.

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 discriminado, 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 computado ou uma computação atrasada que pode ser executada ou permitir iterar por 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 objeto Async representa o trabalho a ser executado para calcular o resultado. Por exemplo, você chama Async.RunSynchronously para executar uma computação 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 de construtor para a expressão de computação e 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 no escopo no início da chave de abertura da expressão de computação. 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.

Como estender construtores com novas operações personalizadas

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

O exemplo a seguir mostra as extensões da classe FSharp.Linq.QueryBuilder existente.

open System
open FSharp.Linq

type QueryBuilder with

    [<CustomOperation>]
    member _.any (source: QuerySource<'T, 'Q>, predicate) =
        System.Linq.Enumerable.Any (source.Source, Func<_,_>(predicate))

    [<CustomOperation("singleSafe")>] // you can specify your own operation name in the constructor 
    member _.singleOrDefault (source: QuerySource<'T, 'Q>, predicate) =
        System.Linq.Enumerable.SingleOrDefault (source.Source, Func<_,_>(predicate))

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 de computação com eficiência

Expressões de computação F# que suspendem a execução podem ser compiladas para computadores de estado altamente eficientes usando de modo cuidadoso um recurso de baixo nível chamado código retomável. O código retomável é documentado no F# RFC FS-1087 e usado para expressões de tarefa.

Expressões de computação F# síncronas (ou seja, não suspendem a execução) podem, como alternativa, ser compiladas para computadores de estado eficientes usando funções embutidas, incluindo o atributo InlineIfLambda. 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 pelo compilador F# para garantir a geração de código de alto desempenho.

Consulte também