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

Вычислительные выражения в F # предоставляют удобный синтаксис для написания вычислений, которые могут быть упорядочены и объединены с помощью конструкций и привязок потока управления. В зависимости от типа вычислительного выражения их можно представить как способ выражения составных, моноидс, нестандартных преобразователей и аппликативе операторов. Однако, в отличие от других языков (например , в 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 { do! ... }
expr { yield ... }
expr { yield! ... }
expr { return ... }
expr { return! ... }
expr { match! ... }

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

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

let!

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

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

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

let! определяется Bind(x, 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 — это элемент для переноса.

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! в вычислительных выражениях.
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! в вычислительных выражениях.
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 |} , преобразуется в вызовы, включающие методы класса построителя с помощью переводов, описанных в следующей таблице. Вычислительное выражение {| 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 -> Exception exn
                match res with
                | Ok cont -> catch cont // note, a tailcall
                | Error exn -> result (Error exn))

    /// The delay operator.
    let delay func = NotYetDone (fun () -> func())

    /// The stepping action for the computations.
    let step expr =
        match expr with
        | Done _ -> expr
        | NotYetDone func -> func ()

    /// The tryFinally operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryFinally expr compensation =
        catch (expr)
        |> bind (fun res ->
            compensation();
            match res with
            | Ok value -> result value
            | Error exn -> raise exn)

    /// The tryWith operator.
    /// This is boilerplate in terms of "result", "catch", and "bind".
    let tryWith exn handler =
        catch exn
        |> bind (function Ok value -> result value | Error exn -> handler exn)

    /// The whileLoop operator.
    /// This is boilerplate in terms of "result" and "bind".
    let rec whileLoop pred body =
        if pred() then body |> bind (fun _ -> whileLoop pred body)
        else result ()

    /// The sequential composition operator.
    /// This is boilerplate in terms of "result" and "bind".
    let combine expr1 expr2 =
        expr1 |> bind (fun () -> expr2)

    /// The using operator.
    /// This is boilerplate in terms of "tryFinally" and "Dispose".
    let using (resource: #System.IDisposable) func =
        tryFinally (func resource) (fun () -> resource.Dispose())

    /// The forLoop operator.
    /// This is boilerplate in terms of "catch", "result", and "bind".
    let forLoop (collection:seq<_>) func =
        let ie = collection.GetEnumerator()
        tryFinally
            (whileLoop
                (fun () -> ie.MoveNext())
                (delay (fun () -> let value = ie.Current in func value)))
            (fun () -> ie.Dispose())

/// The builder class.
type EventuallyBuilder() =
    member x.Bind(comp, func) = Eventually.bind func comp
    member x.Return(value) = Eventually.result value
    member x.ReturnFrom(value) = value
    member x.Combine(expr1, expr2) = Eventually.combine expr1 expr2
    member x.Delay(func) = Eventually.delay func
    member x.Zero() = Eventually.result ()
    member x.TryWith(expr, handler) = Eventually.tryWith expr handler
    member x.TryFinally(expr, compensation) = Eventually.tryFinally expr compensation
    member x.For(coll:seq<_>, func) = Eventually.forLoop coll func
    member x.Using(resource, expr) = Eventually.using resource expr

let eventually = new EventuallyBuilder()

let comp =
    eventually {
        for x in 1..2 do
            printfn $" x = %d{x}"
        return 3 + 4
    }

/// Try the remaining lines in F# interactive to see how this
/// computation expression works in practice.
let step x = Eventually.step x

// returns "NotYetDone <closure>"
comp |> step

// prints "x = 1"
// returns "NotYetDone <closure>"
comp |> step |> step

// prints "x = 1"
// prints "x = 2"
// returns "Done 7"
comp |> step |> step |> step |> step

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

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

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

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

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

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

См. также