使用 clojure.spec 分解地图

Using clojure.spec to decompose a map

我知道 clojure.spec 不是用于任意数据转换,据我了解,它是用于通过任意谓词灵活编码领域知识。这是一个非常强大的工具,我喜欢使用它。

这么多,也许,我 运行 我正在 mergeing 地图,component-acomponent-b,每个都可以许多形式中的一种,变成 composite,然后想 "unmix" 把 composite 变成它的组成部分。

这被建模为组件的两个 multi-spec 和这些组件的 s/merge 的复合:

;; component-a
(defmulti component-a :protocol)
(defmethod component-a :p1 [_]
  (s/keys :req-un [::x ::y ::z]))
(defmethod component-a :p2 [_]
  (s/keys :req-un [::p ::q ::r]))
(s/def ::component-a
  (s/multi-spec component-a :protocol))

;; component-b
(defmulti component-b :protocol)
(defmethod component-b :p1 [_]
  (s/keys :req-un [::i ::j ::k]))
(defmethod component-b :p2 [_]
  (s/keys :req-un [::s ::t]))
(s/def ::component-b
  (s/multi-spec component-b :protocol))

;; composite
(s/def ::composite
  (s/merge ::component-a ::component-b)

我希望能够执行以下操作:

(def p1a {:protocol :p1 :x ... :y ... :z ...})
(def p1b (make-b p1a)) ; => {:protocol :p1 :i ... :j ... :k ...}

(def a (s/conform ::component-a p1a))
(def b (s/conform ::component-b p1b))
(def ab1 (s/conform ::composite (merge a b))

(?Fn ::component-a ab1) ; => {:protocol :p1 :x ... :y ... :z ...}
(?Fn ::component-b ab1) ; => {:protocol :p1 :i ... :j ... :k ...}

(def ab2 {:protocol :p2 :p ... :q ... :r ... :s ... :t ...})
(?Fn ::component-a ab2) ; => {:protocol :p2 :p ... :q ... :r ...}
(?Fn ::component-b ab2) ; => {:protocol :p2 :s ... :t ...}

换句话说,我想重用为 component-acomponent-b 编码的领域知识来分解 composite.

我的第一个想法是将键本身与对 s/keys:

的调用隔离开来
(defmulti component-a :protocol)
(defmethod component-a :p1 [_]
  (s/keys :req-un <form>)) ; <form> must look like [::x ::y ::z]

但是,从 "something else" 计算 s/keys 的键的方法失败了,因为 <form> 必须是 ISeq。也就是说,<form> 既不能是计算 ISeqfn,也不能是表示 ISeq.

symbol

我也尝试过使用 s/describe 在 运行 时动态读取密钥,但这通常不适用于 multi-specs,因为它会使用简单的 s/def。我不会说我用尽了这种方法,但它看起来像是递归 s/describes 的兔子洞并直接访问 multi-specs 底层的 multifns,感觉很脏。

我也想过在:protocol的基础上加一个单独的multifn:

(defmulti decompose-composite :protocol)
(defmethod decompose-composite :p1
  [composite]
  {:component-a (select-keys composite [x y z])
   :component-b (select-keys composite [i j k]))

但这显然没有重用领域知识,它只是复制它并公开应用它的另一种途径。它也特定于 composite;我们需要一个 decompose-other-composite 用于不同的组合。

所以目前这只是一个有趣的谜题。我们总是可以将组件嵌套在组合中,使它们再次隔离变得微不足道:

(s/def ::composite
  (s/keys :req-un [::component-a ::component-b]))
(def ab {:component-a a :component-b b})
(do-composite-stuff (apply merge (vals ab)))

但是有没有更好的方法来实现?Fn呢?自定义 s/conformer 可以做这样的事情吗?还是 merged 图更像是物理混合物,即更难分离?

I also experimented with using s/describe to read the keys dynamically at run-time, but this doesn't work generally with multi-specs as it would with a simple s/def

想到的一个解决方法是定义 s/keys 规范,将 defmethod 的 from/outside 分开,然后取回 s/keys 表单并提取关键字.

;; component-a
(s/def ::component-a-p1-map
  (s/keys :req-un [::protocol ::x ::y ::z])) ;; NOTE explicit ::protocol key added
(defmulti component-a :protocol)
(defmethod component-a :p1 [_] ::component-a-p1-map)
(s/def ::component-a
  (s/multi-spec component-a :protocol))
;; component-b
(defmulti component-b :protocol)
(s/def ::component-b-p1-map
  (s/keys :req-un [::protocol ::i ::j ::k]))
(defmethod component-b :p1 [_] ::component-b-p1-map)
(s/def ::component-b
  (s/multi-spec component-b :protocol))
;; composite
(s/def ::composite (s/merge ::component-a ::component-b))

(def p1a {:protocol :p1 :x 1 :y 2 :z 3})
(def p1b {:protocol :p1 :i 4 :j 5 :k 6})
 (def a (s/conform ::component-a p1a))
(def b (s/conform ::component-b p1b))
(def ab1 (s/conform ::composite (merge a b)))

对于 s/keys 规范的独立规范,您可以使用 s/form:

取回单独的密钥
(defn get-spec-keys [keys-spec]
  (let [unqualify (comp keyword name)
        {:keys [req req-un opt opt-un]}
        (->> (s/form keys-spec)
             (rest)
             (apply hash-map))]
    (concat req (map unqualify req-un) opt (map unqualify opt-un))))

(get-spec-keys ::component-a-p1-map)
=> (:protocol :x :y :z)

然后您可以在合成地图上使用 select-keys

(defn ?Fn [spec m]
  (select-keys m (get-spec-keys spec)))

(?Fn ::component-a-p1-map ab1)
=> {:protocol :p1, :x 1, :y 2, :z 3}

(?Fn ::component-b-p1-map ab1)
=> {:protocol :p1, :i 4, :j 5, :k 6}

并使用您的 decompose-composite 想法:

(defmulti decompose-composite :protocol)
(defmethod decompose-composite :p1
  [composite]
  {:component-a (?Fn ::component-a-p1-map composite)
   :component-b (?Fn ::component-b-p1-map composite)})

(decompose-composite ab1)
=> {:component-a {:protocol :p1, :x 1, :y 2, :z 3},
    :component-b {:protocol :p1, :i 4, :j 5, :k 6}}

However, approaches where the keys of s/keys are computed from "something else" fail because must be an ISeq. That is, can neither be a fn that computes an ISeq, nor a symbol that represents an ISeq.

或者,您可以 eval 以编程方式构建的 s/keys 表单:

(def some-keys [::protocol ::x ::y ::z])
(s/form (eval `(s/keys :req-un ~some-keys)))
=> (clojure.spec.alpha/keys :req-un [:sandbox.core/protocol
                                     :sandbox.core/x
                                     :sandbox.core/y
                                     :sandbox.core/z])

以后直接用some-keys