让我从下面的代码开始:
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#中可修改的词典.
谢谢.
首先,你的直接问题:你定义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)
| 归档时间: |
|
| 查看次数: |
396 次 |
| 最近记录: |