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