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

Выражения вычислений в 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тип -like (определенный элементом 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"
    }

Как и в случае с ключевым словом yield в 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# определяет четыре встроенных вычислительных выражения: выражения последовательности, асинхронные выражения, выражения задач и выражения запросов.

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

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

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

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

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! Вызывается в выражениях вычислений, но повышает эффективность за счет уменьшения числа узлов с подстановками.

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, TryWithTryFinallyи Combine. Тип Delayed<'T> является типом возвращаемого Delay значения и, следовательно, параметром для этих функций. Delayed<'T> может быть произвольным типом, к которому не нужно обращаться M<'T>; обычно M<'T> или (unit -> M<'T>) используются. Реализация по умолчанию— M<'T>. Более подробный взгляд см. здесь .

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

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

В приведенном выше коде вызовы Run и Delay пропускаются, если они не определены в классе построителя выражений вычислений. Текст вычислительного выражения, как показано {| cexpr |}здесь, преобразуется в вызовы, включающие методы класса построителя, переводы, описанные в следующей таблице. Вычислительное выражение {| 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 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()

В предыдущей таблице описывается выражение, 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#, чтобы обеспечить создание высокопроизводительного кода.

См. также