Я понимаю, что clojure.spec
не предназначен для произвольного преобразования данных, и, насколько я понимаю, он предназначен для гибкого кодирования знаний предметной области с помощью произвольных предикатов. Это безумно мощный инструмент, и мне нравится им пользоваться.
Возможно, настолько много, что я столкнулся со сценарием, в котором я merge
объединяю карты, component-a
и component-b
, каждая из которых может принимать одну из многих форм, в composite
, а затем хочу "размешать" composite
в его компонент. части.
Это моделируется как два multi-spec
s для компонентов и 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/describe
s и прямого доступа к multifn
s, лежащим в основе multi-spec
s, что казалось грязным.
Я также думал о добавлении отдельного 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
сделать что-то подобное? Или карты merge
d больше похожи на физические смеси, то есть их непропорционально труднее разделить?