如何编写一个计算表达式生成器,该生成器可以累积值并允许标准语言构造



我有一个计算表达式生成器,它在执行过程中构建一个值,并具有许多自定义操作。然而,它不允许使用标准的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循环的支持,为我指明正确的方向(至少usingwhiletry/catch会很好,但如果有人让我开始,我可能会弄清楚这些),那么我将能够感激地将这一课应用到我的实际问题中。

最好的地方是规范。例如,

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的调用范围内(请将未绑定的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

我见过的最完整的例子在规范的§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

在"F#为乐趣和利润"的教程在这方面是一流的。

http://fsharpforfunandprofit.com/posts/computation-expressions-intro/

经过与Joel类似的斗争(没有发现规范的§6.3.10有帮助),我在让For构造生成列表方面的问题归结为让类型正确排列(不需要特殊属性)。特别是,尽管编译器尽了最大努力纠正了我的错误,但我慢慢意识到For将建立一个列表列表,因此需要扁平化。我在网上发现的例子总是使用yield关键字对seq{}进行包装,重复使用该关键字会调用Combine,从而实现扁平化。如果有一个具体的例子有帮助,下面的摘录使用来构建一个整数列表——我的最终目标是创建用于在GUI中渲染的组件列表(添加了一些额外的惰性)。此外,在这里深入讨论CE,详细阐述了kvb的上述观点。

module scratch
    type Dispatcher = unit -> unit
    type viewElement = int
    type lazyViews = Lazy<list<viewElement>>
    type ViewElementsBuilder() =                
        member x.Return(views: lazyViews) : list<viewElement> = views.Value        
        member x.Yield(v: viewElement) : list<viewElement> = [v]
        member x.ReturnFrom(viewElements: list<viewElement>) = viewElements        
        member x.Zero() = list<viewElement>.Empty
        member x.Combine(listA:list<viewElement>, listB: list<viewElement>) =  List.concat [listA; listB]
        member x.Delay(f) = f()
        member x.For(coll:seq<'a>, forBody: 'a -> list<viewElement>) : list<viewElement>  =         
            // seq {for v in coll do yield! f v} |> List.ofSeq                       
            Seq.map forBody coll |> Seq.collect id  |> List.ofSeq
    let ve = new ViewElementsBuilder()
    let makeComponent(m: int, dispatch: Dispatcher) : viewElement = m
    let makeComponents() : list<viewElement> = [77; 33]
    let makeViewElements() : list<viewElement> =         
        let model = {| Scores = [33;23;22;43;] |> Seq.ofList; Trainer = "John" |}
        let d:Dispatcher = fun() -> () // Does nothing here, but will be used to raise messages from UI
        ve {                        
            for score in model.Scores do
                yield makeComponent (score, d)
                yield makeComponent (score * 100 / 50 , d)
            if model.Trainer = "John" then
                return lazy 
                [ makeComponent (12, d)
                  makeComponent (13, d)
                ]
            else 
                return lazy 
                [ makeComponent (14, d)
                  makeComponent (15, d)
                ]
            yield makeComponent (33, d)        
            return! makeComponents()            
        }

相关内容

  • 没有找到相关文章

最新更新