Using multimethod hierarchies with spec
(ns applesoranges.core | |
(:require [clojure.spec.alpha :as s])) | |
;; define the properties | |
(s/def ::fruit-attribute-spec (s/keys :req [::diameter ::color])) | |
(s/def ::diameter nat-int?) | |
(s/def ::color #{:green :orange}) | |
;; define the "type"-hierarchy | |
;; seemed like a good idea to use a separate spec for the hierarchy (in order to keep things simple) | |
(s/def ::fruit-spec (s/and ::fruit-attribute-spec (s/or ::apple ::apple-spec | |
::orange ::orange-spec))) | |
;; define the "subclasses" | |
;; seems clunky to me, maybe there is a better solution? | |
(s/def ::apple-spec (s/and | |
#(s/int-in-range? 4 9 (::diameter %)) | |
#(#{:orange :green} (::color %)))) | |
(s/def ::orange-spec (s/and | |
#(s/int-in-range? 7 12 (::diameter %)) | |
#(#{:orange} (::color %)))) | |
;; test the spec | |
(s/conform ::fruit-spec {::diameter 8 | |
::color :green}) | |
; => [:applesoranges.core/apple #:applesoranges.core{:diameter 8, :color :green}] | |
(s/conform ::fruit-spec {::diameter 10 | |
::color :orange}) | |
; => [:applesoranges.core/orange #:applesoranges.core{:diameter 10, :color :orange}] | |
;; map turns from ::orange to ::apple as ::diameter becomes smaller (priority defined by the order in s/or) | |
;; Note that the "type" is inferred exclusively based on attributes and not an explicit or implicit class or type | |
(s/conform ::fruit-spec {::diameter 8 | |
::color :orange}) | |
; => [:applesoranges.core/apple #:applesoranges.core{:diameter 8, :color :orange}] | |
;; conform with specs basically tells us whether it's an ::apple or an ::orange | |
;; now we need to do something with those keywords | |
;; define the hierarchy of keywords | |
(def fruit-hierarchy (make-hierarchy)) | |
(defmulti cut (fn [fruit] | |
(->> fruit | |
(s/conform ::fruit-spec) | |
key)) | |
:hierarchy #'fruit-hierarchy) | |
(defmethod cut ::fruit [fruit] | |
(println "Cut into slices: " fruit)) | |
(defmethod cut ::apple [fruit] | |
(println "Cut apple into slices, remove seeds: " fruit)) | |
(cut {::diameter 6 | |
::color :green}) | |
;; Cut apple into slices, remove seeds: #:applesoranges.core{:diameter 6, :color :green} | |
(cut {::diameter 10 | |
::color :orange}) | |
;; CompilerException java.lang.IllegalArgumentException: No method in multimethod 'cut' for dispatch value: :applesoranges.core/orange | |
;; We haven't defined what should happen for ::orange | |
;; "change" hierarchy in place. Only do this when you have to | |
(def fruit-hierarchy (derive fruit-hierarchy ::orange ::apple)) | |
(cut {::diameter 10 | |
::color :orange}) | |
;; Cut apple into slices, remove seeds: #:applesoranges.core{:diameter 10, :color :orange} | |
;; inherited ::apple behavior (as an example) | |
;; supply special implementation | |
(defmethod cut ::orange [fruit] | |
(println "Peel orange: " fruit)) | |
(cut {::diameter 10 | |
::color :orange}) | |
;; Peel orange: #:applesoranges.core{:diameter 10, :color :orange} | |
(comment | |
"Comments about specs" | |
"- specs can parse by content (see conform calls above). This only works, however, for closed specs. | |
I.e. specs with fixed number of entries in s/or. This means the spec cannot be extended. | |
Other code can't introduce new 'subclasses'." | |
"- there is also multi-spec which allows extensible specs. The caveat is that they can no longer parse by content, only by tag. | |
That would require another attribute in the maps, e.g. {:type ::apple ...}." | |
"- specs can be overlapping or ambiguous if you will. The order of specs inside s/or then defines the priorities | |
(first is most important)" | |
"Comments about hierarchies:" | |
"- The hierarchies of multimethod and specs don't have to be the same. Above example derives ::orange from ::apple, | |
even though the spec defines no real hierarchy, only alternatives." | |
"- Hierarchies can be defined per multimethod. A different multimethod might use the same keywords but in a different hierarchy. | |
If no hierarchy is defined, the method uses the default hierarchy." | |
"See also https://feierabendprojekte.wordpress.com/2018/09/23/multimethod-hierarchies-and-spec/") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment