Clojure - 使用 Specter 转换嵌套数据结构,用多个节点替换一个节点

6

我正在使用 specter 在 Clojure 中转换嵌套数据结构,但我还没有掌握它。特别是,我正试图创建一个转换,它将找到与谓词匹配的任何深度的项,并用多个项替换它。

[:top
 [:arbitrary 1 2
  [:nesting
   2
   3
   [:needle] ; <-- the thing to find
   ]]]

-->

[:top
 [:arbitrary 1 2
  [:nesting
   2
   3
   [:n1] [:n2] [:n3]  ; <-- 3 items inserted in the place of 1
   ]]]

我无法弄清楚的是如何将替换项目拼接到父向量中,即如何用三个项目替换一个项目,而不是用包含三个子项的单个项目替换一个项目。
4个回答

3
我不知道如何使用Specter完成这个任务,但是这里有一个使用clojure.zip的函数可以实现:
(defn splice-replace [zipper smap]
  (loop [loc zipper]
    (if (z/end? loc)
      (z/root loc)
      (recur
       (z/next
        (if-let [sub (smap (z/node loc))]
          (reduce (comp z/right z/insert-right)
                  (z/replace loc (first sub))
                  (rest sub))
          loc))))))

你可以使用你的数据结构的拉链和一个从你想要替换的值到它们替换值序列的映射来调用它:
(def zipper
  (z/vector-zip [:top
                 [:arbitrary 1 2
                  [:nesting 2 3 [:needle]]]]))

(splice-replace zipper {[:needle] [[:n1] [:n2] [:n3]]})
 => [:top [:arbitrary 1 2 [:nesting 2 3 [:n1] [:n2] [:n3]]]]

(splice-replace zipper {[:nesting 2 3 [:needle]] (range 3 10)})
=> [:top [:arbitrary 1 2 3 4 5 6 7 8 9]]

1
虽然它不是 Specter,但它能满足我的需求。 - TomSW

3
(defn replace-needle [input replacement]
    (let [needle-parent?     #(= % [:needle])
          NEEDLE-PARENT      (recursive-path
                                 [] p (cond-path
                                          #(and (vector? %) (some needle-parent? %)) [(continue-then-stay [ALL p])]
                                          vector? [ALL p]))
          inject-replacement (fn inject [x] (vec (mapcat #(if (needle-parent? %) replacement [%]) x)))]
        (transform [NEEDLE-PARENT] inject-replacement input)))


(let [input       [:top
                   [:arbitrary 1 2
                    [:nesting 2 3 [:needle]]]]
      replacement [[:n1] [:n2] [:n3]]]
    (replace-needle input replacement))

1
谢谢。我试图创建一个与项本身匹配的路径,然后尝试控制如何在父级中插入替换项,而你正在搜索项的父级并进行转换,这更容易理解。该解决方案并不总是替换所有出现: (让[input[:top[:needle] [:a[:needle]]]replacement[1 2 3]](replace-needle input replacement)) - >[: top 1 2 3 [: a [: needle]]] - TomSW
另一种方法是找到包含[:needle]的向量,然后找到[:needle]的索引,然后使用srange将新元素拼接到该索引处的父级中。在Specter中是否有可能表达“找到包含[:needle]的向量,然后找到[:needle]的索引”而不使用自定义导航器? - user2609980
我把它做成了递归。 - akond

1
我认为可以找到一个包含[:needle]的向量,然后找到[:needle]的索引,并使用srange将新元素拼接到父元素中的该索引处,但我没有找到使用Specter完成此操作的方法。
下面是使用clojure.walk表达相同想法的方式:
(require '[clojure.walk :refer [postwalk]])

(postwalk (fn [node]
            (if (and (vector? node)
                     (some (partial = [:needle]) node))
              (let [idx (.indexOf node [:needle])]
                (vec (concat (take idx node)
                             [[:n1] [:n2] [:n3]]
                             (drop (inc idx) node))))
              node))
          data)

;; => [:top [:arbitrary 1 2 [:nesting 2 3 [:n1] [:n2] [:n3]]]]

1
这是一个有趣的想法。我的目标是将一个简单的结构逐步转换为ooxml(用于Word文档)。在以前的工作中,我可能会使用XSLT,也许有比specter更好的解决方案,但它看起来太有趣了,不试着学习一下就太可惜了。 - TomSW
Specter 应该是可用的。Nathan 的演讲很棒。我曾经涉猎过它。到目前为止,我的经验是:对于通常情况来说,简洁的语法不值得为其他开发人员所不熟悉而付出代价。而对于高级情况,我发现它相当难以阅读,例如自定义导航器、多路径和终端等。为了辩护它,在我的上一份工作中,我们遇到了一个问题,即在普通的 Clojure 中似乎无法解决的单元测试。我的同事开始工作,并在一天内编写了 10 行 Specter 代码来解决它。这是一项艺术品。 - user2609980
也许使用Specter,您可以像README中的示例一样创建自定义的TreeWalker。 :) - user2609980

0

这个解决方案使用递归路径,然后使用srange-dynamic来进行就地更新。在“注入/转换”函数中再次进行线性搜索并不是很好,但似乎没有任何方法可以在当前位置注入类似于BEGINNING或END的方式。

(transform [(recursive-path [] RECURSE
                        (if-path sequential?
                          (if-path (selected? [ALL (pred= [:needle])])
                            STAY
                            [ALL  RECURSE])))]
       (fn inject [v]  (setval (srange-dynamic
                                #(.indexOf % [:needle])
                                (end-fn [_ start] (inc start)))
                               [[:n1] [:n2] [:n3]] v))
       [:top [:arbitrary 1 2 [:nesting 2 3 [:needle] :more]]])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接