Использование clojure.spec для разложения карты - PullRequest
0 голосов
/ 17 октября 2018

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

1 Ответ

0 голосов
/ 17 октября 2018

Я также экспериментировал с использованием s / description для динамического чтения ключей во время выполнения, но это обычно не работает с мульти-спецификациями, как это было бы с простым 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 непосредственно позже.

...