管理函数语言中嵌套不可变数据结构的更新

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)

所有这些更新一个简单的属性是非常难看的,但此外调用者必须组装所有参数!

在处理函数式语言中的不可变数据结构时,这必须是一个相当普遍的要求,所以有一个好的模式或技巧可以避免我应该使用它吗?

zmi*_*ila 7

尝试

(update-in 
  world 
  [country city building] 
  (update-object-in-building object property value))
Run Code Online (Sandbox Code Playgroud)


C. *_*ann 6

这个问题的经典通用解决方案是所谓的"拉链"数据结构.有许多变化,但基本思路很简单:给定一个嵌套的数据结构,在遍历它时将它拆开,这样在每一步你都有一个"当前"元素和一个表示如何重建其余的数据结构"在"当前元素之上.拉链也许可以被认为是一个"光标",它可以通过一个不可变的数据结构,在它去的时候替换它,只重新创建它所必需的部分.

在列表的普通情况下,片段只是列表的前一个元素,以相反的顺序存储,遍历只是将一个列表的第一个元素移动到另一个列表.

在二元树的非常简单但仍然很简单的情况下,片段每个都由一个值和一个子树组成,标识为右或左.将拉链"向左下"移动涉及向片段列表添加当前元素的值和右子元素,使左子元素成为新的当前元素.移动"右下"的工作方式类似,并且通过将当前元素与片段列表上的第一个值和子树组合来完成"向上"移动.

虽然拉链的基本思想非常通用,但为特定数据结构构造拉链通常需要一些专用位,例如自定义遍历或构造操作,以供通用拉链实现使用.

描述zippers原始论文(警告,PDF)给出了OCaml中的示例代码,用于存储具有通过树的显式路径的片段的实现.不出所料,在Haskell的拉链上也可以找到大量的材料.作为构造显式路径和片段列表的替代方法,可以使用continuation在Scheme中实现拉链.最后,Clojure似乎甚至还提供了一个面向树的拉链.


j-g*_*tus 2

据我所知有两种方法:

将多个参数收集在某种易于传递的对象中。例子:

; 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 宏。