处理F#中的.NET通用词典?

时间:2017-05-11 17:37:32

标签: f#

  1. 我不是一名功能性程序员。
  2. 我正在学习F#。
  3. 我在这里遇到了问题。
  4. 让我从下面的代码开始:

    type XmlNode(tagName, innerValue) =
        member this.TagName = tagName
        member this.InnerValue = innerValue
        member this.Atts = Dictionary<string, obj>()
    

    我不使用F#dict,因为(据我所知),一个是readonly,但我显然需要修改我的属性。 所以我真的很难让它成为纯粹的功能方式:

    type XmlNode with member this.WriteTo (output:StringBuilder) = 
        output.Append("<" + this.TagName) |> ignore
        //let writeAtts = 
        //    List.map2 (fun key value -> " " + key + "=" + value.ToString())
     (List.ofSeq this.Atts.Keys) (List.ofSeq this.Atts.Values)
        //    |> List.reduce (fun acc str -> acc + " " + str)
        //output.Append((writeAtts)) |> ignore
        output.Append(">" + this.InnerValue + "</" + this.TagName + ">") |> ignore
        output
    

    我注释掉的代码是我(可能是愚蠢的)尝试使用映射和缩减来连接单个正确格式化字符串中的所有atts。这编译好了。

    但是当我尝试访问我的Atts属性时:

    [<EntryPoint>]
    let main argv = 
        let root = new XmlNode("root", "test")
        root.Atts.Add("att", "val") // trying to add a new KVP
        let output = new StringBuilder()
        printfn "%O" (root.WriteTo(output))
        Console.ReadLine()|>ignore
        0 // return an integer exit code
    

    ...新属性不会出现在Atts属性中,即它保持为空。

    所以: 1)帮助我使我的代码更具功能性。 2)并了解如何处理F#中可修改的词典。

    谢谢。

1 个答案:

答案 0 :(得分:6)

首先,您的直接问题:您定义Atts属性的方式,它不是一个“存储”在某个地方并且可以通过属性访问的值。相反,您的定义意味着“每次有人读取此属性,创建一个新字典并返回它”。这就是你的新属性没有出现在词典中的原因:每次你阅读root.Atts时,它都是一个不同的词典。

要创建具有支持字段和初始值的属性,请使用member val

type XmlNode(...) =
   ...
   member val Atts = Dictionary<string,object>()

现在,回答一些隐含的问题。

第一项业务:“修改属性”和“纯功能”是矛盾的想法。函数式编程意味着不可变数据。什么都没有改变。推进计算的方法是在每一步创建一个新的数据,而不是覆盖前一个。这个基本思想在实践中证明是非常有价值的:更安全的线程,琐碎的“撤销”场景,平凡的并行化,对其他机器的简单分配,甚至通过持久数据结构减少内存消耗。

不变性是非常重要的一点,我恳请你不要瞥一眼。接受它需要精神上的转变。从我自己(以及我认识的其他人)的经验来看,很难从命令式编程中获得,但它非常值得。

第二:不使用类和属性。从技术上讲,面向对象的编程(在消息传递的意义上)与功能并不矛盾,但在实践中使用并在C ++,Java,C#等人,中实现的企业风味是矛盾,因为它强调这种观点,即“方法是改变对象状态的操作”,这是不起作用的(参见上文)。所以最好避免面向对象的结构,至少在你学习的时候。特别是因为F#提供了更好的数据编码方式:

type XmlNode = { TagName: string; InnerValue: string; Atts: (string*string) list }

(注意我的Atts不是字典;我们稍后会谈到这一点)

同样,要表示对数据的操作,请使用函数,而不是方法:

 let printNode (node: XmlNode) = (* we'll come to the implementation later *)

第三:为什么你说“显然”需要修改属性?您展示的代码并不需要这样做。例如,使用我上面的XmlNode定义,我可以这样重写代码:

[<EntryPoint>]
let main argv = 
    let root = { TagName = "root"; InnerValue = "test"; Atts = ["att", "val"] }
    printfn "%s" (printNode root)
    ...

但即使这是一个真正的需要,你也不应该“到位”。正如我在讨论不变性时所描述的那样,你不应该改变现有节点,而是创建一个与你想要“修改”的方式不同的新节点:

let addAttr node name value = { node with Atts = (name, value) :: node.Atts }

在此实现中,我获取属性的节点和名称/值,并生成 new 节点,其Atts列表包含原始节点{{1}中的任何内容使用新属性前置。

原始Atts列表保持不变,未经修改。但这并不意味着内存消耗的两倍:因为我们知道原始列表永远不会改变,我们可以重用它:我们通过仅为新项目分配内存并将旧列表的引用包括为“其他项目”来创建新列表”。如果旧列表可能会发生变化,我们无法执行此操作,我们必须创建完整副本(请参阅“Defensive Copy”)。此策略称为“Persistent Data Structure”。它是函数式编程的支柱之一。

最后,表示字符串格式,我建议使用Atts代替sprintf。它提供类似的性能优势,但另外提供类型安全性。例如,代码StringBuilder将无法编译,抱怨格式需要字符串,但最终参数sprintf "%s" 5是一个数字。有了这个,我们可以实现5函数:

printNode

供参考,这是您的完整程序,以功能样式重写:

 let printNode (node: XmlNode) =
    let atts = seq { for n, v in node.Atts -> sprintf " %s=\"%s\"" n v } |> String.concat ""
    sprintf "<%s%s>%s</%s>" node.TagName atts node.InnerValue node.TagName