13

我有一个计算表达式构建器,可以随时建立一个值,并且有许多自定义操作。但是,它不允许标准的 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循环的支持来为我指明正确的方向(至少 - usingwhile并且try/catch会很棒,但如果有人让我开始,我可能会弄清楚这些)然后我将能够感激地把这个教训应用到我的实际问题上。

4

4 回答 4

13

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

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(将 unboundx视为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
于 2014-04-17T04:12:37.290 回答
3

我见过的最完整的例子在规范的§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
于 2014-04-17T03:07:24.617 回答
2

“F# for fun and profit”的教程在这方面是一流的。

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

于 2014-08-24T14:44:44.397 回答
1

在与 Joel 进行了类似的斗争之后(并且没有找到有帮助的规范的 §6.3.10),我让 For 构造生成列表的问题归结为让类型正确排列(不需要特殊属性)。特别是我很慢地意识到 For 会构建一个列表列表,因此需要展平,尽管编译器尽最大努力让我正确。我在网上找到的示例总是围绕 seq{} 进行包装,使用 yield 关键字,重复使用它会调用对 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()            
        }


于 2020-05-13T14:31:58.340 回答