Как написать построитель выражений вычислений, который накапливает значение, а также позволяет создавать стандартные языковые конструкции?

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

Чтобы дать автономный пример, это мертвое простое и довольно бессмысленное выражение вычисления, которое строит списки F #:

type Items<'a> = Items of 'a list

type ListBuilder() =
    member x.Yield(()) = Items []

    [<CustomOperation("add")>]
    member x.Add(Items current, item:'a) =
        Items [ yield! current; yield item ]

    [<CustomOperation("addMany")>]
    member x.AddMany(Items current, items: seq<'a>) =
        Items [ yield! current; yield! items ]

let listBuilder = ListBuilder()

let build (Items items) = items

Я могу использовать это для создания списков просто отлично:

let stuff =
    listBuilder {
        add 1
        add 5
        add 7
        addMany [ 1..10 ]
        add 42
    } 
    |> build

Однако это ошибка компилятора:

listBuilder {
    let x = 5 * 39
    add x
}

// This expression was expected to have type unit, but
// here has type int.

И вот так:

listBuilder {
    for x = 1 to 50 do
        add x
}

// This control construct may only be used if the computation expression builder
// defines a For method.

Я прочитал всю документацию и примеры, которые я могу найти, но там что-то я просто не получаю. Каждой подписи .Bind() или .For(), которую я пытаюсь, просто приводит к все более запутывающим ошибкам компилятора. Большинство примеров я могу найти либо построить значение по мере продвижения, либо разрешить регулярные конструкторы языка F #, но я не смог найти тот, который делает оба.

Если кто-то может указать мне в правильном направлении, показывая мне, как взять этот пример и добавить поддержку в построителе для привязок let и for циклов (минимум - using, while и try/catch было бы здорово, но я, вероятно, смогу понять это, если кто-то меня начнет), тогда я с благодарностью смогу применить урок к моей реальной проблеме.

Ответ 1

Лучшее место для поиска - spec. Например,

b {
    let x = e
    op x
}

переводится на

   T(let x = e in op x, [], fun v -> v, true)
=> T(op x, {x}, fun v -> let x = e in v, true)
=> [| op x, let x = e in b.Yield(x) |]{x}
=> b.Op(let x = e in in b.Yield(x), x)

Итак, это показывает, где все пошло не так, хотя оно не представляет собой очевидного решения. Очевидно, что Yield необходимо обобщить, так как ему нужно взять произвольные кортежи (в зависимости от того, сколько переменных в области). Возможно, более тонко, это также показывает, что x не находится в области видимости в вызове add (см., Что unbound x в качестве второго аргумента b.Op?). Чтобы позволить вашим пользовательским операторам использовать связанные переменные, их аргументы должны иметь атрибут [<ProjectionParameter>] (и принимать функции от произвольных переменных в качестве аргументов), и вам также нужно установить MaintainsVariableSpace в true, если вы хотите, чтобы связанный переменные будут доступны для более поздних операторов. Это изменит окончательный перевод на:

b.Op(let x = e in b.Yield(x), fun x -> x)

Исходя из этого, кажется, что нет способа избежать передачи связанных значений по каждой операции (хотя я бы хотел, чтобы ее доказали неправильно) - это потребует, чтобы вы добавили Run чтобы удалить эти значения в конце. Соединяя все это, вы получите создателя, который выглядит так:

type ListBuilder() =
    member x.Yield(vars) = Items [],vars

    [<CustomOperation("add",MaintainsVariableSpace=true)>]
    member x.Add((Items current,vars), [<ProjectionParameter>]f) =
        Items (current @ [f vars]),vars

    [<CustomOperation("addMany",MaintainsVariableSpace=true)>]
    member x.AddMany((Items current, vars), [<ProjectionParameter>]f) =
        Items (current @ f vars),vars

    member x.Run(l,_) = l

Ответ 2

Наиболее полные примеры, которые я видел, находятся в §6.3.10 спецификации, особенно этот:

/// Computations that can cooperatively yield by returning a continuation
type Eventually<'T> =
    | Done of 'T
    | NotYetDone of (unit -> Eventually<'T>)

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Eventually =

    /// The bind for the computations. Stitch 'k' on to the end of the computation.
    /// Note combinators like this are usually written in the reverse way,
    /// for example,
    ///     e |> bind k
    let rec bind k e =
        match e with
        | Done x -> NotYetDone (fun () -> k x)
        | NotYetDone work -> NotYetDone (fun () -> bind k (work()))

    /// The return for the computations.
    let result x = Done x

    type OkOrException<'T> =
        | Ok of 'T
        | Exception of System.Exception                    

    /// The catch for the computations. Stitch try/with throughout
    /// the computation and return the overall result as an OkOrException.
    let rec catch e =
        match e with
        | Done x -> result (Ok x)
        | NotYetDone work ->
            NotYetDone (fun () ->
                let res = try Ok(work()) with | e -> Exception e
                match res with
                | Ok cont -> catch cont // note, a tailcall
                | Exception e -> result (Exception e))

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

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

    // The rest of the operations are boilerplate.

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

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

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

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

    /// The using operator.
    let using (resource: #System.IDisposable) f =
        tryFinally (f resource) (fun () -> resource.Dispose())

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


// Give the mapping for F# computation expressions.
type EventuallyBuilder() =
    member x.Bind(e,k)                  = Eventually.bind k e
    member x.Return(v)                  = Eventually.result v   
    member x.ReturnFrom(v)              = v   
    member x.Combine(e1,e2)             = Eventually.combine e1 e2
    member x.Delay(f)                   = Eventually.delay f
    member x.Zero()                     = Eventually.result ()
    member x.TryWith(e,handler)         = Eventually.tryWith e handler
    member x.TryFinally(e,compensation) = Eventually.tryFinally e compensation
    member x.For(e:seq<_>,f)            = Eventually.forLoop e f
    member x.Using(resource,e)          = Eventually.using resource e