Reputation: 1549
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?
Upvotes: 2
Views: 139
Reputation: 55185
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.
Upvotes: 5
Reputation: 36718
Computation expressions can be difficult to use until you fully understand how they work. If you're relatively new to F#, I'd suggest going about it without the computation expressions, using plain function calls and lists to construct your nodes. Something like the following:
type Node =
{
Key: Option<string>
Children: List<Node>
XPathFromParent: string
}
let defaultNode =
{
Key = None;
Children = [];
XPathFromParent = ".//somePath"
}
let withNodes children node = { node with Children = children }
let withXpathFromParent xpath node = { node with XPathFromParent = xpath }
let withKey key node = { node with Key = Some key }
let mkNode xpath children = { Key = None
Children = children
XPathFromParent = xpath }
// Usage example
let rootNode =
mkNode ".//somePath" [
mkNode "somepath" [] |> withKey "childkey1"
mkNode "someOtherPath" [] // No key specified, so this one will be None
] |> withKey "parentKey"
That produces a rootNode
that looks like this:
val rootNode : Node =
{Key = Some "parentKey";
Children =
[{Key = Some "childkey1";
Children = [];
XPathFromParent = "somepath";}; {Key = null;
Children = [];
XPathFromParent = "someOtherPath";}];
XPathFromParent = ".//somePath";}
Upvotes: 4