mik*_*era 13 language-agnostic design-patterns functional-programming clojure immutability
我注意到在追求精益函数式编程的过程中,当使用嵌套的不可变数据结构时,参数列表开始变得过多.这是因为在更新对象状态时,您还需要更新数据结构中的所有父节点.请注意,这里我将"更新"表示"返回具有适当更改的新不可变对象".
例如,我发现自己编写的函数(Clojure示例)是:
(defn update-object-in-world [world country city building object property value]
(update-country-in-world world
(update-city-in-country country
(update-building-in-city building
(update-object-in-building object property value)))))
Run Code Online (Sandbox Code Playgroud)
所有这些更新一个简单的属性是非常难看的,但此外调用者必须组装所有参数!
在处理函数式语言中的不可变数据结构时,这必须是一个相当普遍的要求,所以有一个好的模式或技巧可以避免我应该使用它吗?
尝试
(update-in
world
[country city building]
(update-object-in-building object property value))
Run Code Online (Sandbox Code Playgroud)
这个问题的经典通用解决方案是所谓的"拉链"数据结构.有许多变化,但基本思路很简单:给定一个嵌套的数据结构,在遍历它时将它拆开,这样在每一步你都有一个"当前"元素和一个表示如何重建其余的数据结构"在"当前元素之上.拉链也许可以被认为是一个"光标",它可以通过一个不可变的数据结构,在它去的时候替换它,只重新创建它所必需的部分.
在列表的普通情况下,片段只是列表的前一个元素,以相反的顺序存储,遍历只是将一个列表的第一个元素移动到另一个列表.
在二元树的非常简单但仍然很简单的情况下,片段每个都由一个值和一个子树组成,标识为右或左.将拉链"向左下"移动涉及向片段列表添加当前元素的值和右子元素,使左子元素成为新的当前元素.移动"右下"的工作方式类似,并且通过将当前元素与片段列表上的第一个值和子树组合来完成"向上"移动.
虽然拉链的基本思想非常通用,但为特定数据结构构造拉链通常需要一些专用位,例如自定义遍历或构造操作,以供通用拉链实现使用.
描述zippers的原始论文(警告,PDF)给出了OCaml中的示例代码,用于存储具有通过树的显式路径的片段的实现.不出所料,在Haskell的拉链上也可以找到大量的材料.作为构造显式路径和片段列表的替代方法,可以使用continuation在Scheme中实现拉链.最后,Clojure似乎甚至还提供了一个面向树的拉链.
据我所知有两种方法:
将多个参数收集在某种易于传递的对象中。例子:
; world is a nested hash, the rest are keys
(defstruct location :world :country :city :building)
(defstruct attribute :object :property)
(defn do-update[location attribute value]
(let [{:keys [world country city building]} location
{:keys [object property]} attribute ]
(update-in world [country city building object property] value)))
Run Code Online (Sandbox Code Playgroud)
这使您只剩下调用者需要关心的两个参数(位置和属性),如果这些参数不经常更改,这可能是足够公平的。
另一种选择是 with-X 宏,它设置代码体使用的变量:
(defmacro with-location [location & body] ; run body in location context
(concat
(list 'let ['{:keys [world country city building] :as location} `~location])
`(~@body)))
Example use:
(with-location location (println city))
Run Code Online (Sandbox Code Playgroud)
然后,无论主体做什么,它都会对其设置的世界/国家/城市/建筑物进行操作,并且它可以使用“预组装”参数将整个事情传递给另一个函数location。
更新:现在有了一个实际有效的 with-location 宏。