Skip to content

Instantly share code, notes, and snippets.

Last active Oct 26, 2018
What would you like to do?
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)
: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}
"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")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment