Clojure의 트리 데이터 구조에서 Spectre 사용

비주얼 앱을 만드는 것은 항상 재미있었고, 제가 즐길 수 있는 앱을 만드는 것을 고려하고 있었습니다. 고민 끝에 마인드 매핑 도구를 만들기로 했습니다. 나는 최근에 KonvaJSkonva-cljs를 위한 간단한 ClojureScript 래퍼를 작성했기 때문에 이것이 아주 적합해야 한다고 생각했습니다!

자세히 살펴보면 마인드 매핑 구조가 트리 데이터 구조라는 것을 즉시 알 수 있습니다. 즉, 깊이 중첩된 구조를 처리해야 하고 작업에 적합한 도구가 필요합니다. 가장 먼저 떠오르는 것은 Specter 이라는 멋진 라이브러리입니다.

더 이상 고민하지 않고 몇 가지 코드를 확인하겠습니다. (프로젝트에서 제공한 코드 샘플)

CRUD 기능 중 하나는 id로 노드를 찾는 것이었습니다. 그래서 저는 이것을 생각해 냈습니다.

(require '[com.rpl.specter :as s])

(defn find-node-by-id [tree id]
  (s/select-first (s/walker #(= id (:id %))) tree))


Walker는 pred 함수가 진실 값을 반환하는 노드에 대해 깊이 우선 탐색을 실행합니다. pred가 참 값을 반환하면 Walker는 트리의 해당 분기 검색을 중지하고 나머지 데이터 구조 검색을 계속합니다.

모든 노드에는 고유한 id 속성이 있으므로 select-first 매크로를 사용하여 첫 번째 노드를 선택할 수 있습니다.

또한 자식 ID로 부모 노드를 찾아야 했습니다.

(defn find-parent-by-child-id [tree child-id]
  (s/select-first
   (s/walker
    (fn [node]
      ((set (map :id (:children node))) child-id)))
   tree))


언젠가는 같은 수준의 노드를 찾아야 합니다.

(defn find-same-level-nodes [tree node-id]
  (let [parent (find-parent-by-child-id tree node-id)]
    (if (:root? parent)
      (:children parent)
      (->> parent
           :id
           (find-parent-by-child-id tree)
           :children
           (mapcat :children)))))


트리에서 일부 노드를 제거하고 업데이트해 보겠습니다. 그렇게 하기 전에 트리를 순회하기 위해 recursive-path를 정의할 수 있습니다. (또 다른 접근 방식)

(def MAP-NODES
  (s/recursive-path [] p
    (s/cond-path
      sequential? (s/continue-then-stay s/ALL p)
      map? (s/continue-then-stay s/MAP-VALS p))))


Spectre는 순차적이거나 지도인 데이터를 발견하면 MAP-VALS에 대한 데이터 검색으로 재귀적으로 이동합니다. (우리는 coll? 만 사용할 수도 있습니다)

자, 이 중요한 경로를 정의한 후 노드를 추가해 보겠습니다.

(defn add-node [tree parent-id node id]
  (let [node (assoc node :id id :h shape-h :w shape-w)
        tree (s/transform [q/MAP-NODES #(= parent-id (:id %)) :children]
                          #((fnil conj []) % node)
                          tree)]
    (update-positions tree parent-id id)))

add-node 는 MAP-NODES 를 사용하여 트리를 탐색하고 주어진 parent-id 를 찾으면 제공된 노드가 :children 에 추가됩니다.

제거 작업;

(defn remove-node-by-id [tree id]
  (s/setval [MAP-NODES #(= id (:id %))] s/NONE tree))


여기에서는 setval 매크로를 사용하여 노드를 제거합니다.

업데이트도 비슷합니다.

(defn update-node-by-id [tree id update-fn]
  (s/transform [MAP-NODES #(= id (:id %))] update-fn tree))


업데이트 기능 때문에 transform 대신 setval를 사용했습니다.

중첩된 모든 자식을 부모 ID로 업데이트합시다. 먼저 모든 노드를 가져오는 함수를 만들어 보겠습니다.

(defn- traverse [node]
  (when (-> node :children seq)
    (lazy-cat (:children node) (map traverse (:children node)))))

(defn get-nodes [root]
  (->> root
       (traverse)
       (cons root)
       (flatten)
       (remove nil?)))


이제 모든 자식에게 업데이트 작업을 적용할 수 있습니다.

(defn update-children-by-parent-id [tree parent-id update-fn]
  (let [parent   (find-node-by-id tree parent-id)
        children (set (map :id (rest (get-nodes parent))))]
    (s/transform [MAP-NODES #(children (:id %))] update-fn tree)))


포스팅을 너무 길게 하고 싶지 않습니다. 나는 당신이 전체 그림을 가지고 있다고 생각합니다.

간단히 말해서 Spectre는 강력하고 재미있게 작업할 수 있습니다. 잘 구조화된 추상화로 상당히 복잡한 문제를 해결할 수 있습니다.

저와 비슷한 사용 사례가 있다면 적극 추천합니다.

좋은 웹페이지 즐겨찾기