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

Ser*_*bov 3 f#

  1. 我不是一个功能程序员.
  2. 我正在学习F#.
  3. 我在这里遇到了问题.

让我从下面的代码开始:

type XmlNode(tagName, innerValue) =
    member this.TagName = tagName
    member this.InnerValue = innerValue
    member this.Atts = Dictionary<string, obj>()
Run Code Online (Sandbox Code Playgroud)

我不使用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
Run Code Online (Sandbox Code Playgroud)

我注释掉的代码是我(可能是愚蠢的)尝试使用映射和缩减来连接单个正确格式化字符串中的所有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
Run Code Online (Sandbox Code Playgroud)

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

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

谢谢.

Fyo*_*kin 7

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

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

type XmlNode(...) =
   ...
   member val Atts = Dictionary<string,object>()
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

type XmlNode = { TagName: string; InnerValue: string; Atts: (string*string) list }
Run Code Online (Sandbox Code Playgroud)

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

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

 let printNode (node: XmlNode) = (* we'll come to the implementation later *)
Run Code Online (Sandbox Code Playgroud)

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

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

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

let addAttr node name value = { node with Atts = (name, value) :: node.Atts }
Run Code Online (Sandbox Code Playgroud)

在此实现中,我获取属性的节点和名称/值,并生成一个节点,该节点的Atts列表包含原始节点中Atts具有前置新属性的任何内容.

原始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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

   Console.ReadLine() |> ignore
   0
Run Code Online (Sandbox Code Playgroud)