我认识到clojure.spec
并非用于任意数据转换,而且据我所知,它旨在通过任意谓词灵活地编码域知识。这是一个非常强大的工具,我喜欢使用它。
也许太多了,以至于我遇到了merge
地图component-a
和component-b
的情况,每种地图都可以采用多种形式之一composite
,然后又想将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-a
和component-b
编码的领域知识,以分解composite
。
我的第一个想法是将密钥本身与对s/keys
的调用隔离开来:
(defmulti component-a :protocol)
(defmethod component-a :p1 [_]
(s/keys :req-un <form>)) ; <form> must look like [::x ::y ::z]
但是,由于s/keys
必须是<form>
,因此根据“其他”来计算ISeq
的键的方法会失败。也就是说,<form>
既不能是计算fn
的{{1}},也不能是表示ISeq
的{{1}}。
我还尝试了使用symbol
在运行时动态读取键,但这通常不适用于ISeq
,就像简单的s/describe
一样。我不会说我用尽了这种方法,但是它看起来像是一个递归multi-specs
并直接访问s/def
下的s/describe
底层的multifn
的兔子洞。
我还考虑过基于multi-spec
添加单独的multifn
:
:protocol
但是,这显然不会重用领域知识,它只是复制知识并提供了另一种应用知识的途径。它也特定于一个(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
d地图更像是物理混合物,也就是说,很难分开?
答案 0 :(得分:3)
我还尝试了使用s / describe在运行时动态读取密钥,但这通常不适用于多规范,就像简单的s / def一样。
想到的一种变通办法是定义s/keys
之外的defmethod
规范,然后重新获得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}}
但是,从“其他”计算s / key的键的方法失败,因为它必须是ISeq。也就是说,既不能是计算ISeq的fn,也不能是表示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
。