Использование clojure.spec для декомпозиции карты

Я понимаю, что clojure.spec не предназначен для произвольного преобразования данных, и, насколько я понимаю, он предназначен для гибкого кодирования знаний предметной области с помощью произвольных предикатов. Это безумно мощный инструмент, и мне нравится им пользоваться.

Возможно, настолько много, что я столкнулся со сценарием, в котором я mergeобъединяю карты, component-a и component-b, каждая из которых может принимать одну из многих форм, в composite, а затем хочу "размешать" composite в его компонент. части.

Это моделируется как два multi-specs для компонентов и 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, вычисляющим ISeq, ни symbol, представляющим ISeq.

Я также экспериментировал с использованием s/describe для динамического чтения ключей во время выполнения, но обычно это не работает с multi-specs, как с простым s/def. Я не скажу, что исчерпал этот подход, но он казался кроличьей норой рекурсивных s/describes и прямого доступа к multifns, лежащим в основе multi-specs, что казалось грязным.

Я также думал о добавлении отдельного 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 сделать что-то подобное? Или карты merged больше похожи на физические смеси, то есть их непропорционально труднее разделить?


person Nolan330    schedule 17.10.2018    source источник


Ответы (1)


Я также экспериментировал с использованием 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/keys вычисляются из «чего-то еще», терпят неудачу, поскольку должны быть ISeq. То есть не может быть ни fn, вычисляющим ISeq, ни символом, представляющим 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 непосредственно позже.

person Taylor Wood    schedule 17.10.2018