使用clojure.spec分解地图

时间:2018-10-17 19:42:25

标签: clojure clojure.spec

我认识到clojure.spec并非用于任意数据转换,而且据我所知,它旨在通过任意谓词灵活地编码域知识。这是一个非常强大的工具,我喜欢使用它。

也许太多了,以至于我遇到了merge地图component-acomponent-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-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]

但是,由于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地图更像是物理混合物,也就是说,很难分开?

1 个答案:

答案 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