使用 clojure.spec 分解地图
Using clojure.spec to decompose a map
我知道 clojure.spec
不是用于任意数据转换,据我了解,它是用于通过任意谓词灵活编码领域知识。这是一个非常强大的工具,我喜欢使用它。
这么多,也许,我 运行 我正在 merge
ing 地图,component-a
和 component-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-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]
但是,从 "something else" 计算 s/keys
的键的方法失败了,因为 <form>
必须是 ISeq
。也就是说,<form>
既不能是计算 ISeq
的 fn
,也不能是表示 ISeq
.
的 symbol
我也尝试过使用 s/describe
在 运行 时动态读取密钥,但这通常不适用于 multi-specs
,因为它会使用简单的 s/def
。我不会说我用尽了这种方法,但它看起来像是递归 s/describe
s 的兔子洞并直接访问 multi-spec
s 底层的 multifn
s,感觉很脏。
我也想过在: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
可以做这样的事情吗?还是 merge
d 图更像是物理混合物,即更难分离?
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
我知道 clojure.spec
不是用于任意数据转换,据我了解,它是用于通过任意谓词灵活编码领域知识。这是一个非常强大的工具,我喜欢使用它。
这么多,也许,我 运行 我正在 merge
ing 地图,component-a
和 component-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-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]
但是,从 "something else" 计算 s/keys
的键的方法失败了,因为 <form>
必须是 ISeq
。也就是说,<form>
既不能是计算 ISeq
的 fn
,也不能是表示 ISeq
.
symbol
我也尝试过使用 s/describe
在 运行 时动态读取密钥,但这通常不适用于 multi-specs
,因为它会使用简单的 s/def
。我不会说我用尽了这种方法,但它看起来像是递归 s/describe
s 的兔子洞并直接访问 multi-spec
s 底层的 multifn
s,感觉很脏。
我也想过在: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
可以做这样的事情吗?还是 merge
d 图更像是物理混合物,即更难分离?
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