I'm trying to use computational expressions to create a builder-like DSL, but when I try to use let assignments to help compose things, I get a compilation error that such assignments cannot be found. Here's an example:
type Node =
{
Key: Option<string>
Children: List<Node>
XPathFromParent: string
}
let defaultNode =
{
Key = None;
Children = [];
XPathFromParent = ".//somePath"
}
type NodeBuilder(xpath: string) =
member self.Yield(item: 'a): Node = defaultNode
member this.xpath = xpath
[<CustomOperation("xpath_from_parent")>]
member __.XPathFromParent (node, x) = {node with XPathFromParent = x}
[<CustomOperation("nodes")>]
member __.Nodes (node, x) = {node with Children = x}
[<CustomOperation("key")>]
member __.MidasMeasurementKey (node, x) = {node with Key = x}
member this.Bind(x, f) = f x
let node xpath = NodeBuilder(xpath)
let rootNode = node ".//somePath" {
let! childNodes =
[
node "somepath" {
nodes []
};
node "someOtherPath" {
nodes []
}
]
nodes childNodes // The value or constructor 'childNodes' is not defined.
}
How can I alter this code so that I can reference the childNodes assignment to pass it into the nodes custom operator?
Your immediate problem is that you need to put a [<ProjectionParameter>] attribute on any arguments to custom operators that you wish to be able to access the variable space of the computation expression. However, once you add this, you'll find that you have some problems with mismatched types. In general, I agree with rmunn: computation expressions are not necessarily a good fit for your problem, so you should strongly consider using a different mechanism instead.
However, if you insist on pushing onwards, here's one trick to help you debug. It looks like you want to be able to write
node "something" {
let! childNodes = ([some expression]:Node list)
nodes childNodes
}
So create a dummy builder like this (the seemingly useless Quote method is the key):
type DummyNodeBuilder(xpath:string) =
[<CustomOperation("nodes")>]
member __.Nodes (node:Node, [<ProjectionParameter>]x) = node // Note: ignore x for now and pass node through unchanged
member __.Yield(_) = Unchecked.defaultof<_> // Note: don't constrain types at all
member __.Bind(_,_) = Unchecked.defaultof<_> // Note: don't constrain types at all
member __.Quote() = ()
let node xpath = DummyNodeBuilder xpath
let expr =
node "something" {
let! childNodes = [] : Node list
nodes childNodes
}
and you'll see that expr holds a quotation roughly equivalent to:
builder.Nodes(
builder.Bind([],
fun childNodes -> builder.Yield childNodes),
fun childNodes -> childNodes)
so in your real builder you'll need to have methods that have compatible signatures (e.g. Nodes's second argument must accept a function, and the first argument must be compatible with the return type of Bind, etc.). As you try out other workflows you'd like to enable with the dummy builder, you can see how they desugar and discover additional constraints.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With