Существует ли канонический или идиоматический способ написания спецификации для зависимостей между значениями в узлах в рекурсивно определенной структуре данных?
В качестве минимального примера предположим, что я хочу сохранить отсортированный список в виде вложенного вектора, где каждый «узел» является значением и хвостом списка:
[1 [2 [3 [4 nil]]]]
Спецификацию структуры самого списка можно написать
(s/def ::node (s/or :empty nil?
:list (s/cat :value number? :tail ::node)))
Однако, когда я хочу добавить требование к порядку, я не могу найти хороший способ писать это.
Прямой способ написания кажется немного неуклюжим. Как согласовано
значение :tail
— это MapEntry, я не могу использовать что-то вроде (get-in % [:tail :list :value])
(Я мог бы написать это как (get-in % [:tail 1 :value])
, но этот жестко закодированный индекс кажется слишком хрупким),
но нужно продеть через (val)
:
(s/def ::node (s/or :empty nil?
:list (s/& (s/cat :value number? :tail ::node)
#(or (= (-> % :tail key) :empty)
(< (:value %) (-> % :tail val :value)))
)))
Это работает:
(s/conform ::node nil) ; [:empty nil]
(s/conform ::node [1 nil ] ) ; [:list {:value 1, :tail [:empty nil]}]
(s/explain ::node [4 [1 nil]] )
; {:value 4, :tail [:list {:value 1, :tail [:empty nil]}]} - failed:
; (or (= (-> % :tail key) :empty) (< (:value %) (-> % :tail val
; :value))) in: [1] at: [:list] spec: :spec-test.core/node
; [4 [1 nil]] - failed: nil? at: [:empty] spec: :spec-test.core/node
(s/conform ::node [1 [4 nil]] ) ; [:list {:value 1, :tail [:list {:value 4, :tail [:empty nil]}]}]
(s/conform ::node [1 [2 [4 nil]]] )
; [:list
; {:value 1,
; :tail
; [:list {:value 2, :tail [:list {:value 4, :tail [:empty nil]}]}]}]
В качестве альтернативы я могу использовать мультиспецификацию, чтобы сделать спецификацию для ::node
немного понятнее:
(s/def ::node (s/or :empty nil?
:list (s/& (s/cat :value number? :tail ::node)
(s/multi-spec empty-or-increasing :ignored)
)))
Это также позволяет мне отделить ветку :empty
, но у нее все еще есть проблема с получением значения (головы) :tail
:
(defmulti empty-or-increasing #(-> % :tail key))
(defmethod empty-or-increasing :empty
[_]
(fn[x] true))
(defmethod empty-or-increasing :default
[_]
#(do (< (:value %) (-> % :tail val :value)))
)
Есть ли способ получить :value
узла :tail
без необходимости
извлеките val
часть MapEntry
с помощью #(-> % :tail val :value)
или #(get-in % [:tail 1 :value])
?
Вы можете использовать s/conformer
, чтобы получить карту вместо MapEntry.
(s/def ::node (s/and (s/or :empty nil?
:list (s/& (s/cat :value number? :tail ::node)
(fn [x]
(or (-> x :tail (contains? :empty))
(-> x :tail :list :value (> (:value x)))))))
(s/conformer (fn [x] (into {} [x])))))
и результат будет выглядеть несколько более последовательным:
(s/conform ::node [1 [2 [4 nil]]])
=> {:list {:value 1, :tail {:list {:value 2, :tail {:list {:value 4, :tail {:empty nil}}}}}}}