让我从下面的代码开始:
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#中可修改的词典。
谢谢。
答案 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