Skip to content

Instantly share code, notes, and snippets.

@Azel4231
Last active September 16, 2022 13:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Azel4231/53b610befc62085a87666bb812ec118e to your computer and use it in GitHub Desktop.
Save Azel4231/53b610befc62085a87666bb812ec118e to your computer and use it in GitHub Desktop.
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