nebffa
nebffa

Reputation: 1549

Cannot re-use variable assigned within a computation expression

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

Answers (2)

kvb
kvb

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

rmunn
rmunn

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

Related Questions