【问题标题】:Cannot re-use variable assigned within a computation expression无法重用在计算表达式中分配的变量
【发布时间】:2017-02-24 05:28:48
【问题描述】:

我正在尝试使用计算表达式来创建类似构建器的 DSL,但是当我尝试使用 let 分配来帮助组合事物时,我收到一个编译错误,即无法找到此类分配。这是一个例子:

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.
}

如何更改此代码,以便我可以引用 childNodes 赋值以将其传递给 nodes 自定义运算符?

【问题讨论】:

    标签: f#


    【解决方案1】:

    您的直接问题是您需要将[&lt;ProjectionParameter&gt;] 属性放在您希望能够访问计算表达式的变量空间的自定义运算符的任何参数上。然而,一旦你添加了这个,你会发现你有一些类型不匹配的问题。总的来说,我同意rmunn:计算表达式不一定适合您的问题,因此您应该强烈考虑使用不同的机制。

    但是,如果您坚持向前推进,这里有一个技巧可以帮助您进行调试。看起来你希望能够写作

    node "something" {
        let! childNodes = ([some expression]:Node list)
        nodes childNodes
    }
    

    所以像这样创建一个 dummy builder(看似无用的Quote 方法是关键):

    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
        }
    

    您会看到expr 包含一个大致相当于:

    builder.Nodes(
        builder.Bind([], 
                     fun childNodes -> builder.Yield childNodes),
        fun childNodes -> childNodes)
    

    因此,在您的真实构建器中,您需要具有具有兼容签名的方法(例如,Nodes 的第二个参数必须接受一个函数,并且第一个参数必须与 Bind 的返回类型兼容,等等.)。当您尝试使用虚拟构建器启用的其他工作流程时,您可以看到它们如何脱糖并发现其他约束。

    【讨论】:

      【解决方案2】:

      在您完全了解它们的工作原理之前,可能很难使用计算表达式。如果您对 F# 比较陌生,我建议您不要使用计算表达式,而是使用普通函数调用和列表来构建您的节点。类似于以下内容:

      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"
      

      这会产生一个看起来像这样的rootNode

      val rootNode : Node =
        {Key = Some "parentKey";
         Children =
          [{Key = Some "childkey1";
            Children = [];
            XPathFromParent = "somepath";}; {Key = null;
                                             Children = [];
                                             XPathFromParent = "someOtherPath";}];
         XPathFromParent = ".//somePath";}
      

      【讨论】:

      • 嘿@rmunn,你猜对了,我是 F# 的新手 :)。但是,我真的特别在寻找如何使用计算表达式来做到这一点。我正在尝试做的是创建一个构建器 DSL,即使是非技术人员也可以针对我公司遇到的特定业务问题编写它——在我看来,带有计算表达式的假设版本更容易理解。如果仅针对技术人员,我会按照您明智的建议去做。
      • 那我就帮不了你了:我可以重现你的问题,但它也难倒我。我能提出的最佳建议是将 printfn 语句(又名穷人的调试器 :-) 洒到您的构建器中,看看它们何时(以及是否)被调用。
      • 但我的另一个建议是,如果您想为非技术人员构建 DSL,计算表达式可能不如使用 FParsec 滚动您自己的小语言。即使对于经验丰富的开发人员,计算表达式的错误消息也往往有点不透明;非技术人员会发现 不可能 弄清楚他们(说)遗漏了一个词(例如,他们应该写 nodes [] 时写了 [])。而如果您使用 FParsec 为他们提供自定义语言来编写,您可以控制语法和错误消息。
      • @nebffa:根据我的经验,您可以获得一个非常好的 DSL,方法是拥有一个带有短(甚至可能是一个字母)名称的静态类,以及一堆重载方法或带有可选参数的方法。我确实看到了您尝试做的事情的吸引力(我过去也尝试过类似的事情),但它有其自身的限制,而且意外增加的复杂性根本不值得。
      猜你喜欢
      • 2013-05-14
      • 2018-12-07
      • 1970-01-01
      • 1970-01-01
      • 2021-09-08
      • 1970-01-01
      • 2017-01-02
      • 2018-04-17
      相关资源
      最近更新 更多