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