Выражения вычисления

Выражения вычислений в F# предоставляют удобный синтаксис для написания вычислений, которые можно выполнить последовательно и объединить с помощью конструкций потока управления и привязок. В зависимости от типа вычислительного выражения они могут рассматриваться как способ выражения монадов, моноид, преобразователей monad и применимых функторов. Однако в отличие от других языков (таких как нотация do-notation в Haskell), они не привязаны к одной абстракции и не полагаются на макросы или другие формы метапрограммирования для выполнения удобного и контекстно-конфиденциального синтаксиса.

Обзор

Вычисления могут принимать множество форм. Наиболее распространенная форма вычисления — однопоточное выполнение, которое легко понять и изменить. Однако не все формы вычислений столь же просты, как однопоточное выполнение. Некоторыми примерами могут служить:

  • Недетерминированные вычисления
  • Асинхронные вычисления
  • Эффектные вычисления
  • Создание вычислений

В целом существуют контекстно-чувствительные вычисления, которые необходимо выполнить в определенных частях приложения. Написание контекстно-конфиденциального кода может быть сложной задачей, так как легко "утечка" вычислений вне заданного контекста без абстракций, чтобы предотвратить это. Эти абстракции часто сложно писать самостоятельно, поэтому F# имеет обобщенный способ сделать так называемые выражения вычислений.

Выражения вычислений предлагают единую модель синтаксиса и абстракции для вычисления с учетом контекста.

Каждое выражение вычисления поддерживается типом построителя . Тип построителя определяет операции, доступные для выражения вычислений. См. статью "Создание нового типа выражения вычисления", в котором показано, как создать пользовательское вычислительное выражение.

Общие сведения о синтаксисе

Все выражения вычислений имеют следующую форму:

builder-expr { cexper }

В этой форме builder-expr — это имя типа построителя, который определяет выражение вычисления и cexper является телом выражения вычисления. Например, async код выражения вычислений может выглядеть следующим образом:

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

        let processedData = processData data

        return processedData
    }

Существует специальный дополнительный синтаксис, доступный в выражении вычислений, как показано в предыдущем примере. Следующие формы выражений возможны с помощью выражений вычислений:

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

Каждая из этих ключевое слово и другие стандартные ключевое слово F# доступны только в выражении вычислений, если они определены в типе резервного построителя. Единственное исключение из этого заключается match!в том, что сам синтаксический сахар используется для использования let! шаблона совпадения по результату.

Тип построителя — это объект, определяющий специальные методы, определяющие способ объединения фрагментов выражения вычисления; то есть его методы управляют поведением выражения вычислений. Другой способ описать класс построителя заключается в том, чтобы сказать, что он позволяет настраивать операцию многих конструкций F#, таких как циклы и привязки.

let!

Ключевое слово let! привязывает результат вызова к другому вычислительному выражению с именем:

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

Если привязать вызов к вычислительному выражению let, вы не получите результат вычисления. Вместо этого вы привязали значение нереализованного вызова к такому вычислительному выражению. Используется let! для привязки к результату.

let! определяется элементом Bind(x, f) типа построителя.

and!

Ключевое слово and! позволяет привязать результаты нескольких вызовов выражений вычислений с помощью выполнения.

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

Использование ряда сил повторного выполнения дорогостоящих let! ... let! ... привязок, поэтому следует let! ... and! ... использовать при привязке результатов многочисленных выражений вычислений.

and! определяется главным образом элементом MergeSources(x1, x2) типа построителя.

При необходимости можно определить, MergeSourcesN(x1, x2 ..., xN) чтобы уменьшить количество узлов tupling, а BindN(x1, x2 ..., xN, f)BindNReturn(x1, x2, ..., xN, f) также определить или определить для эффективной привязки результатов вычисления без создания туплинга узлов.

do!

do! Ключевое слово используется для вызова вычислительного выражения, возвращающего unitтип типа типа (определенный Zero членом в построителе):

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

Для асинхронного рабочего процесса этот тип имеет тип Async<unit>. Для других выражений вычислений тип, скорее всего, будет CExpType<unit>.

do! определяется элементом Bind(x, f) в типе построителя, где f создается unitобъект.

yield

Ключевое слово yield предназначено для возврата значения из вычислительного выражения, чтобы его можно было использовать в качествеIEnumerable<T>:

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

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

В большинстве случаев это может быть опущено вызывающими абонентами. Самый распространенный способ опущений yield заключается в операторе -> :

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

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

Для более сложных выражений, которые могут дать множество различных значений, и, возможно, условно, просто опустить ключевое слово может сделать:

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

Как и в случае с ключевое слово доходности в C#, каждый элемент в выражении вычисления возвращается по мере итерации.

yield определяется элементом Yield(x) в типе построителя, где x является элемент, возвращаемый обратно.

yield!

Ключевое слово yield! предназначено для выравнивания коллекции значений из вычислительного выражения:

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

При вычислении выражение вычислений, вызываемое путем yield! , будет иметь его элементы, возвращающие один к одному, сглаживая результат.

yield! определяется элементом YieldFrom(x) в типе построителя, где x находится коллекция значений.

В отличие от yieldэтого, yield! необходимо явно указать. Его поведение не является неявным в выражениях вычислений.

return

Ключевое слово return упаковывает значение в тип, соответствующий выражению вычисления. Помимо выражений вычислений, используемых yield, он используется для завершения вычисления выражения:

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 определяется элементом Return(x) в типе построителя, где x находится элемент для упаковки. Для let! ... return использования BindReturn(x, f) можно использовать для повышения производительности.

return!

Ключевое слово return! реализует значение вычислительного выражения и выполняет оболочку, которая приводит к типу, соответствующему выражению вычислений:

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

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

return! определяется ReturnFrom(x) членом в типе построителя, где x является другое вычислительное выражение.

match!

Ключевое слово match! позволяет встраивать вызов к другому вычислительному выражению и сопоставлению шаблонов в результатах:

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

При вызове вычислительного выражения match!с ним будет реализован результат вызова, как let!. Это часто используется при вызове вычислительного выражения, в котором результат является необязательным.

Встроенные выражения вычислений

Базовая библиотека F# определяет четыре встроенных вычислительных выражения: выражения последовательности, асинхронные выражения, выражения задач и выражения запросов.

Создание нового типа выражения вычислений

Вы можете определить характеристики собственных выражений вычислений, создав класс построителя и определив определенные специальные методы класса. Класс построителя может при необходимости определить методы, перечисленные в следующей таблице.

В следующей таблице описаны методы, которые можно использовать в классе построителя рабочих процессов.

Method Типичные подписи Description
Bind M<'T> * ('T -> M<'U>) -> M<'U> Вызывается и let!do! в выражениях вычислений.
BindN (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Вызывается для эффективного let! и and! в вычислительных выражениях без объединения входных данных.

например Bind3, Bind4.
Delay (unit -> M<'T>) -> Delayed<'T> Упаковывает вычислительное выражение как функцию. Delayed<'T> может быть любым типом M<'T> , обычно или unit -> M<'T> используется. Реализация по умолчанию возвращает M<'T>значение .
Return 'T -> M<'T> return Вызывается в выражениях вычислений.
ReturnFrom M<'T> -> M<'T> return! Вызывается в выражениях вычислений.
BindReturn (M<'T1> * ('T1 -> 'T2)) -> M<'T2> Вызывается для эффективного let! ... return вычисления выражений.
BindNReturn (M<'T1> * M<'T2> * ... * M<'TN> * ('T1 * 'T2 ... * 'TN -> M<'U>)) -> M<'U> Вызывается для эффективного let! ... and! ... return вычисления выражений без объединения входных данных.

например Bind3Return, Bind4Return.
MergeSources (M<'T1> * M<'T2>) -> M<'T1 * 'T2> and! Вызывается в выражениях вычислений.
MergeSourcesN (M<'T1> * M<'T2> * ... * M<'TN>) -> M<'T1 * 'T2 * ... * 'TN> and! Вызывается в выражениях вычислений, но повышает эффективность за счет уменьшения числа узлов tupling.

например MergeSources3, MergeSources4.
Run Delayed<'T> -> M<'T> или

M<'T> -> 'T
Выполняет вычислительное выражение.
Combine M<'T> * Delayed<'T> -> M<'T> или

M<unit> * M<'T> -> M<'T>
Вызывается для последовательности в выражениях вычислений.
For seq<'T> * ('T -> M<'U>) -> M<'U> или

seq<'T> * ('T -> M<'U>) -> seq<M<'U>>
Вызывается для for...do выражений в выражениях вычислений.
TryFinally Delayed<'T> * (unit -> unit) -> M<'T> Вызывается для try...finally выражений в выражениях вычислений.
TryWith Delayed<'T> * (exn -> M<'T>) -> M<'T> Вызывается для try...with выражений в выражениях вычислений.
Using 'T * ('T -> M<'U>) -> M<'U> when 'T :> IDisposable Вызывается для use привязок в выражениях вычислений.
While (unit -> bool) * Delayed<'T> -> M<'T>Или

(unit -> bool) * Delayed<unit> -> M<unit>
Вызывается для while...do выражений в выражениях вычислений.
Yield 'T -> M<'T> Вызывается для yield выражений в выражениях вычислений.
YieldFrom M<'T> -> M<'T> Вызывается для yield! выражений в выражениях вычислений.
Zero unit -> M<'T> Вызывается для пустых else ветвей выражений if...then в вычислительных выражениях.
Quote Quotations.Expr<'T> -> Quotations.Expr<'T> Указывает, что выражение вычисления передается Run члену в виде кавычки. Он преобразует все экземпляры вычисления в кавычки.

Многие методы в классе построителя используют и возвращают конструкцию M<'T> , которая обычно представляет собой отдельный определенный тип, характеризующий тип объединенных вычислений, например Async<'T> для асинхронных выражений и Seq<'T> рабочих процессов последовательности. Сигнатуры этих методов позволяют объединять и вложены друг с другом, чтобы объект рабочего процесса, возвращаемый из одной конструкции, можно передать в следующее.

Многие функции используют результат в качестве аргумента Delay : Run, While, TryWith, TryFinallyи Combine. Тип Delayed<'T> — это возвращаемый тип Delay и, следовательно, параметр для этих функций. Delayed<'T> может быть произвольным типом, который не должен быть связан с M<'T>; обычно M<'T> или (unit -> M<'T>) используется. Реализация по умолчанию M<'T>. Дополнительные сведения см . здесь .

Компилятор при анализе вычислительного выражения преобразует выражение в ряд вызовов вложенных функций с помощью методов в предыдущей таблице и кода в выражении вычисления. Вложенное выражение имеет следующую форму:

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

В приведенном выше коде вызовы Run и Delay опущены, если они не определены в классе построителя выражений вычислений. Текст выражения вычисления, который здесь обозначается как {{ cexpr }}, преобразуется в дальнейшие вызовы методов класса построителя. Этот процесс определяется рекурсивно в соответствии с переводами в следующей таблице. Код в двойных скобках {{ ... }} остается переведенным, expr представляет выражение F# и cexpr представляет выражение вычисления.

Expression Перевод текста
{{ 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()

В предыдущей таблице other-expr описывается выражение, которое не указано в таблице. Класс построителя не должен реализовывать все методы и поддерживать все переводы, перечисленные в предыдущей таблице. Эти конструкции, которые не реализованы, недоступны в выражениях вычислений этого типа. Например, если вы не хотите поддерживать use ключевое слово в выражениях вычислений, можно опустить определение Use в классе построителя.

В следующем примере кода показано вычислительное выражение, инкапсулирующее вычисление в виде ряда шагов, которые можно оценить один шаг за раз. Тип различаемого объединения кодирует OkOrExceptionсостояние ошибки выражения, как было оценено до сих пор. Этот код демонстрирует несколько типичных шаблонов, которые можно использовать в выражениях вычислений, таких как стандартные реализации некоторых методов построителя.

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

Выражение вычисления имеет базовый тип, который возвращает выражение. Базовый тип может представлять вычисляемый результат или отложенное вычисление, которое может быть выполнено, или он может предоставить способ итерации через некоторый тип коллекции. В предыдущем примере базовый тип был Eventually<_>. Для выражения последовательности базовый тип имеет тип System.Collections.Generic.IEnumerable<T>. Для выражения запроса базовый тип .System.Linq.IQueryable Для асинхронного выражения базовый тип .Async Объект Async представляет работу, которую необходимо выполнить для вычисления результата. Например, вы вызываете Async.RunSynchronously выполнение вычисления и возвращаете результат.

Пользовательские операции

Вы можете определить пользовательскую операцию в выражении вычисления и использовать пользовательскую операцию в качестве оператора в выражении вычисления. Например, можно включить оператор запроса в выражение запроса. При определении пользовательской операции необходимо определить методы Yield и For в выражении вычислений. Чтобы определить пользовательскую операцию, поместите его в класс построителя для выражения вычисления, а затем примените его CustomOperationAttribute. Этот атрибут принимает строку в качестве аргумента, который является именем, используемым в пользовательской операции. Это имя входит в область в начале открывающей фигурной скобки выражения вычисления. Поэтому не следует использовать идентификаторы, которые имеют то же имя, что и пользовательская операция в этом блоке. Например, избегайте использования идентификаторов, таких как all или last в выражениях запросов.

Расширение существующих построителей с помощью новых пользовательских операций

Если у вас уже есть класс построителя, его пользовательские операции можно расширить за пределами этого класса построителя. Расширения должны быть объявлены в модулях. Пространства имен не могут содержать члены расширения, кроме одного файла и той же группы объявлений пространства имен, в которой определен тип.

В следующем примере показано расширение существующего 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

Пользовательские операции могут быть перегружены. Дополнительные сведения см. в статье F# RFC FS-1056. Разрешить перегрузки пользовательских ключевое слово в выражениях вычислений.

Эффективное компиляция выражений вычислений

Выражения вычислений F#, которые приостанавливают выполнение, можно скомпилировать на высокопроизводительных компьютерах состояний с помощью низкоуровневой функции, называемой возобновлением кода. Возобновление кода задокументировано в F# RFC FS-1087 и используется для выражений задач.

Выражения вычислений F#, синхронные (т. е. они не приостанавливают выполнение) можно скомпилировать в эффективные компьютеры состояний с помощью встроенных функций , включая InlineIfLambda атрибут. Примеры приведены в F# RFC FS-1098.

Выражения списка, выражения массива и выражения последовательности получают специальное обращение компилятором F#, чтобы обеспечить создание высокопроизводительного кода.

См. также