├── .clj-kondo ├── config.edn └── test.clj ├── resources └── clj-kondo.exports │ └── net.clojars.john │ └── dispacio │ ├── dispacio │ └── alpha │ │ └── core.clj_kondo │ └── config.edn ├── .gitignore ├── deps.edn ├── LICENSE.txt ├── src └── dispacio │ └── alpha │ └── core.cljc └── README.md /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../resources/clj-kondo.exports/net.clojars.john/dispacio"]} 2 | -------------------------------------------------------------------------------- /.clj-kondo/test.clj: -------------------------------------------------------------------------------- 1 | (ns test 2 | (:require 3 | [clojure.edn :as edn] 4 | [dispacio.alpha.core :refer [defp]])) 5 | 6 | (defp my-inc string? [x] (inc (edn/read-string x))) 7 | 8 | (my-inc "1") 9 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/net.clojars.john/dispacio/dispacio/alpha/core.clj_kondo: -------------------------------------------------------------------------------- 1 | (ns dispacio.alpha.core) 2 | 3 | (defmacro defp [sym pred arg-vec & body] 4 | `(do (declare ~sym) 5 | (defmethod ~sym ~pred ~arg-vec ~@body))) 6 | 7 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/net.clojars.john/dispacio/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks {:macroexpand {dispacio.alpha.core/defp dispacio.alpha.core/defp}} 2 | :config-in-call {dispacio.alpha.core/defp {:linters {:unresolved-symbol {:exclude [read-string]}}}}} 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebel_readline_history 2 | .cpcache/ 3 | .DS_Store 4 | pom.xml 5 | *jar 6 | /lib/ 7 | /classes/ 8 | /out/ 9 | /target/ 10 | .lein-deps-sum 11 | .lein-repl-history 12 | .lein-plugins/ 13 | .repl 14 | .nrepl-port 15 | .idea/ 16 | tau.alpha.iml 17 | .cache 18 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.0-RC5"} 3 | org.clojure/clojurescript {:mvn/version "1.10.439"}} 4 | :aliases 5 | {:test 6 | {:extra-paths ["test"] 7 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"} 8 | io.github.cognitect-labs/test-runner 9 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}} 10 | :build {:deps {io.github.seancorfield/build-clj 11 | {:git/tag "v0.3.1" :git/sha "996ddfa"}} 12 | :ns-default build}}} 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 John M Newman III 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/dispacio/alpha/core.cljc: -------------------------------------------------------------------------------- 1 | (ns dispacio.alpha.core 2 | (:require [clojure.string :as str]) 3 | #?(:cljs (:require-macros [dispacio.alpha.core]))) 4 | 5 | ;; state 6 | (def global-polies (atom {})) 7 | 8 | 9 | ;; host utils 10 | (defn- get-class [C] 11 | #?(:clj (class C) 12 | :cljs (-> C .-constructor .-name))) 13 | 14 | (defn- stringify-class [C] 15 | #?(:clj (apply str (drop 5 (remove #{\space} (str C)))) 16 | :cljs (str C))) 17 | 18 | 19 | ;; state utils 20 | (defn- get-namespace [poly] 21 | (->> poly str (drop 2) (apply str) keyword namespace)) 22 | 23 | (defn <-state [poly] 24 | (let [poly-ns (get-namespace poly)] 25 | (-> @global-polies (get poly)))) 26 | 27 | 28 | ;; preferable heirarchy utils 29 | 30 | ; work on isa? hierarchies in cljs 31 | (defn- args-are? [disp args] 32 | (or (isa? (vec args) (->> (first disp) #?(:cljs (mapv str)))) 33 | (isa? (mapv get-class args) (->> (first disp) #?(:cljs (mapv str)))))) 34 | 35 | ; (defn- args-are? [disp args] 36 | ; (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp)))) 37 | 38 | (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first)) 39 | 40 | (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2] 41 | (if-let [p (-> @(-> poly <-state) (get-in [:prefer v1]))] 42 | (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1)))) 43 | 44 | (defn- default-sort [v1 v2] 45 | (if (= v1 :poly/default) 46 | 1 47 | (if (= v2 :poly/default) 48 | -1 49 | 0))) 50 | 51 | (defn- pref [poly v1 v2] 52 | (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2))) 53 | -1 54 | (default-sort v1 v2))) 55 | 56 | (defn- sort-disp [poly] 57 | (if-let [sort-fn (-> @(-> poly <-state) :sort-fn)] 58 | (sort-fn poly) 59 | (swap! (-> poly <-state) update :dispatch 60 | #(->> % (sort-by first (partial pref poly)) vec)))) 61 | 62 | ;; public prefer function 63 | (defn prefer [poly v1 v2] 64 | (swap! (-> poly <-state) update-in [:prefer v1] #(-> % (or #{}) (conj v2))) 65 | (sort-disp poly) 66 | nil) 67 | 68 | 69 | ;; dispatch management 70 | (defn- get-disp [poly filter-fn] 71 | (when-let [state (-> poly <-state)] 72 | (when (= (type (atom nil)) (type state)) 73 | (-> @state (get :dispatch) reverse (->> (filter filter-fn)) first)))) 74 | 75 | (defn- pred->disp [poly pred] 76 | (get-disp poly #(-> % first (= pred)))) 77 | 78 | (defn- pred->poly-fn [poly pred] 79 | (-> poly (pred->disp pred) second)) 80 | 81 | (defn- check-args-length [disp args] 82 | ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2))) 83 | 84 | (defn- check-dispatch-on-args [disp args] 85 | (if (-> disp first vector?) 86 | (-> disp (args-are? args)) 87 | (-> disp first (apply args)))) 88 | 89 | (defn- disp*args? [disp args] 90 | (and (check-args-length disp args) 91 | (check-dispatch-on-args disp args))) 92 | 93 | (defn- args->poly-fn [poly args] 94 | (-> poly (get-disp #(disp*args? % args)) second)) 95 | 96 | 97 | ;; manage dispatch for a given function 98 | (defn poly-impl [poly args] 99 | (if-let [poly-fn (-> poly (args->poly-fn args))] 100 | (-> poly-fn (apply args)) 101 | (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))] 102 | (-> default-poly-fn (apply args)) 103 | (throw 104 | (ex-info 105 | (str "No dispatch in polymethod " 106 | (-> poly <-state deref :sym) 107 | " for arguments: " 108 | (apply pr-str args)) 109 | {}))))) 110 | 111 | (defn- remove-disp [poly pred] 112 | (when-let [disp (pred->disp poly pred)] 113 | (swap! (-> poly <-state) update :dispatch #(->> % (remove #{disp}) vec)))) 114 | 115 | (defn- til& [args] 116 | (count (take-while (partial not= '&) args))) 117 | 118 | (defn- add-disp [poly poly-fn pred params] 119 | (swap! (-> poly <-state) update :dispatch 120 | #(-> % (or []) (conj [pred poly-fn (til& params) (filter #{'&} params)])))) 121 | 122 | (defn setup-poly [poly poly-fn pred params] 123 | (remove-disp poly pred) 124 | (add-disp poly poly-fn pred params) 125 | (sort-disp poly)) 126 | 127 | ;; fancy up the function names 128 | (defn- classify-key [k] 129 | (str (namespace k) "-" (name k))) 130 | 131 | (defn- fix-classes-and-keywords [v] 132 | (if (keyword? v) 133 | (classify-key v) 134 | (stringify-class v))) 135 | 136 | (defn- mk-poly-name [poly-name pred params] 137 | (let [parameters (apply str (interpose "_" params)) 138 | param-name (-> parameters 139 | (str/replace " " "_")) 140 | pred-name (if (vector? pred) 141 | (apply str (interpose "_" (mapv fix-classes-and-keywords pred))) 142 | (if (keyword? pred) 143 | (classify-key pred) 144 | "")) 145 | pred-name (str/replace pred-name "." "_") 146 | pname (-> poly-name str (str/split #"/") (#(str (first %) "-" (second %))))] 147 | (symbol (str "<" pname "><" pred-name "><" param-name ">")))) 148 | 149 | (defn in-cljs? [env] 150 | (boolean (:ns env))) 151 | 152 | ;; setup new poly for new function, called by defp if necessary 153 | (defmacro defpoly [poly-name & [init]] 154 | (let [pname (-> poly-name str (str/split #"/")) 155 | plyname (symbol 156 | (if-not (= 2 (count pname)) 157 | (first pname) 158 | (second pname)))] 159 | `(do 160 | (let [state# (atom (or ~init {})) 161 | old-fn# (if-let [ofn# (-> ~poly-name quote resolve)] 162 | (if ~(in-cljs? &env) 163 | ofn# 164 | #?(:cljs (-> ~poly-name #?(:cljs quote) resolve) 165 | :clj ~(-> poly-name #?(:cljs quote) resolve)))) 166 | poly-sym# (symbol (str ~(str (ns-name *ns*)) "/" (-> ~poly-name quote)))] 167 | (defn ~plyname {:poly true} [& args#] 168 | (poly-impl ~poly-name args#)) 169 | (swap! global-polies assoc ~poly-name state#) 170 | (swap! (-> ~poly-name <-state) assoc :sym poly-sym#) 171 | (when old-fn# 172 | (setup-poly ~poly-name old-fn# :poly/default [])))))) 173 | 174 | ;; (defp my-inc string? [s] (inc (r/read-string s))) 175 | (defmacro defp [poly-name pred params & body] 176 | (if-not (-> poly-name #?(:cljs quote) resolve) 177 | `(do 178 | (when-not (-> ~poly-name quote resolve meta :poly) 179 | (defpoly ~poly-name)) 180 | (let [poly-fn# (fn ~(mk-poly-name poly-name pred params) ~params ~@body)] 181 | (setup-poly ~poly-name poly-fn# ~pred (quote ~params)) 182 | ~poly-name)) 183 | `(do 184 | (let [poly-fn# (fn ~(mk-poly-name poly-name pred params) ~params ~@body)] 185 | (setup-poly ~poly-name poly-fn# ~pred (quote ~params)) 186 | ~poly-name)))) 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dispacio 2 | 3 | dispacio is an _"predicate stack dispatch system"_ for Clojure/Script. 4 | 5 | ## Table of Contents 6 | 7 | * [What](#what) 8 | * [Getting Started](#getting-started) 9 | * [Polymethods](#polymethods) 10 | * [Deriving isa? Dispatch](#deriving-isa?-dispatch) 11 | * [Prefer Dispatch Functions](#preferring-dispatch-functions) 12 | * [Spec Validation Dispatch](#spec-validation-dispatch) 13 | * [Function Extension](#function-extension) 14 | * [Choose Your Own Adventure](#choose-your-own-adventure) 15 | * [Bugs](#bugs) 16 | * [Help!](#help) 17 | 18 | ## What 19 | 20 | dispacio is a very simple _predicate dispatch system_ for Clojure and Clojurescript. 21 | 22 | [Predicate dispatch](https://en.wikipedia.org/wiki/Predicate_dispatch) systems usually offer a comprehensive mechanism that helps users efficiently match against different aspects of arguments being passed to polymorphic functions. 23 | 24 | In _predicate *stack* dispatch_, on the other hand, dispatch predicates are tried in the opposite order that they are defined and the first to return a truthy value wins. 25 | 26 | While dispacio provides no sophisticated matching conveniences out of the box, you do get most of what multimethods provides you: `isa?` hierarchies and `prefer`ing one [polymethod](#polymethods) over another explicitly. 27 | 28 | ## Getting Started 29 | ``` clojure 30 | :deps {net.clojars.john/dispacio {:mvn/version "0.1.0-alpha.4"}} 31 | ``` 32 | For the purposes of this tutorial, require dispacio in a REPL and refer `defp`. 33 | ``` clojure 34 | (require '[dispacio.alpha.core :refer [defp]]) 35 | ``` 36 | 37 | ## Polymethods 38 | 39 | Polymethods are similar to Clojure's [_multimethods_](https://clojure.org/reference/multimethods). In Clojure, `multimethod` definitions take static values to be matched against the return value of a single dispatch function, provided in a `defmulti`. 40 | ```clojure 41 | (defmulti my-inc class) 42 | (defmethod my-inc Number [n] (inc n)) 43 | ``` 44 | With `polymethods` each method can have its own dispatch function. 45 | ``` clojure 46 | (defp my-inc number? [x] (inc x)) 47 | (my-inc 1) 48 | ;#_=> 2 49 | ``` 50 | `1` is passed directly to the `number?` function. 51 | 52 | Unlike with defmethods, all params are passed to each polymethod's dispatch function. 53 | 54 | #### A Canonical Example 55 | Let's look at a more involved example, inspired by the [Clojure documentation on multimethods](https://clojure.org/about/runtime_polymorphism). 56 | 57 | First, a helper function: 58 | ```clojure 59 | (defn are-species? [& animals-species] 60 | (->> animals-species 61 | (partition 2) 62 | (map (fn [[animal species]] (= species (:Species animal)))) 63 | (into #{}) 64 | (= #{true}))) 65 | ``` 66 | Then, a series of encounters based on heterogeneous conditions: 67 | ```clojure 68 | (defp encounter #(are-species? %1 :Bunny %2 :Lion) 69 | [b l] 70 | :run-away) 71 | 72 | (defp encounter #(and (:tired %1) 73 | (are-species? %1 :Bunny %2 :Lion)) 74 | [b l] 75 | :hide) 76 | 77 | (defp encounter #(are-species? %1 :Lion %2 :Bunny) 78 | [l b] 79 | :eat) 80 | 81 | (defp encounter #(and (:tired %1) 82 | (are-species? %1 :Lion %2 :Bunny)) 83 | [l b] 84 | :play) 85 | 86 | (defp encounter #(= (:Species %1) (:Species %2)) 87 | [b1 b2] 88 | :mate) 89 | 90 | (defp encounter #(and (or (:angry %1) (:angry %2)) 91 | (are-species? %1 :Lion %2 :Lion)) 92 | [l1 l2] 93 | :fight) 94 | ``` 95 | Then let's try it out: 96 | ```clojure 97 | (def b1 {:Species :Bunny :tired true}) 98 | (def b2 {:Species :Bunny :other :stuff}) 99 | (def l1 {:Species :Lion :tired true}) 100 | (def l2 {:Species :Lion :angry true}) 101 | 102 | (encounter b1 b2) 103 | ;#_=> :mate 104 | (encounter b1 l1) 105 | ;#_=> :hide 106 | (encounter b2 l1) 107 | ;#_=> :run-away 108 | (encounter l1 b1) 109 | ;#_=> :play 110 | (encounter l2 b1) 111 | ;#_=> :eat 112 | (encounter l1 l2) 113 | ;#_=> :fight 114 | (encounter l1 (assoc l2 :angry false)) 115 | ;#_=> :mate 116 | ``` 117 | Notice that a `polymethod`'s predicate functions are evaluated in the opposite order they are defined. In the example above, the condition `(= (:Species %1) (:Species %2))` will catch all cases where the species is the same, causing them to `:mate`. The `:fight`ing lions condition is then defined, which shadows the _same species_ logic of the prior condition, but only when a lion is angry. 118 | 119 | This is an important distinction between a `polymethod` and a `defmethod`: The static values associated with a `defmethod`, checked against the return value of a `defmulti`, make a set that will match exclusively of one another. `polymethod`s on the other hand can be defined disjointly or their predicative scope can overlap. A more generally defined predicate can shadow a more specifically defined predicate if it was defined after the more specifically defined predicate. Therefore, it is probably best to define your more general, catch-all predicates earlier rather than later. That is, unless we want a more general case to short-curcuit the rest of the stack, like we did with the `:mate`ing example - we could have defined that one first, but this way we don't have to check all the other predicates before deciding to `:mate`. Useful for when you're in a rush. However, our `:tired` scenarios had to be defined after their more general `:run-away` and `:eat` scenarios, otherwise they would have been fully shadowed and prevented from catching the condition. 120 | 121 | #### Mutual Recursion 122 | There's lots of interesting things you can do with predicate dispatch. Here's a cool recursive definition of zipmap I copped from [this paper on predicate dispatch](https://homes.cs.washington.edu/~mernst/pubs/dispatching-ecoop98.pdf): 123 | ```clojure 124 | (defp zip-map #(or (empty? %1) (empty? %2)) 125 | [_ _] 126 | nil) 127 | 128 | (defp zip-map #(and (seq %1) (seq %2)) 129 | [a b] 130 | (apply merge 131 | {(first a) (first b)} 132 | (zip-map (rest a) (rest b)))) 133 | 134 | (zip-map (range 10) (range 10)) 135 | ;#_=> {0 0, 7 7, 1 1, 4 4, 6 6, 3 3, 2 2, 9 9, 5 5, 8 8} 136 | ``` 137 | Now, you wouldn't want to actually do that to replace actual `zipmap`, as it'll run slower and you'll blow your stack for large sequences. But the point is that you can construct mutually recursive definitions with `polymethods` to create interesting algorithms. 138 | 139 | #### Across Namespaces 140 | 141 | Imagine you want to expose a low-level email function and a high level `polymethod` from a namespace called `emails`: 142 | ```clojure 143 | (ns emails 144 | (:require [dispacio.alpha.core :refer [defp]])) 145 | 146 | (defn post-email! [email] 147 | (println :sending-email :msg (:msg email))) 148 | 149 | (defp send! :poly/default 150 | [email] 151 | (println :don't-know-what-to-do-with-this email)) 152 | ``` 153 | You can then implement the polymethods across namespaces of different domains: 154 | ```clojure 155 | (ns promotion 156 | (:require [dispacio.alpha.core :refer [defp]] 157 | [emails :as emails])) 158 | 159 | (defp emails/send! #(-> % :email-type (= :promotion)) 160 | [email] 161 | (emails/post-email! 162 | (assoc email 163 | :msg (str "Congrats! You got a promotion " (:name email) "!")))) 164 | ``` 165 | ```clojure 166 | (ns welcome 167 | (:require [dispacio.alpha.core :refer [defp]] 168 | [emails :as emails])) 169 | 170 | (defp emails/send! #(-> % :file-type namespace (= "welcome")) 171 | [email] 172 | (emails/post-email! 173 | (assoc email 174 | :msg (str "Welcome! Glad you're here " (:name email) "!")))) 175 | ``` 176 | ```clojure 177 | (ns confirmation 178 | (:require [dispacio.alpha.core :refer [defp]] 179 | [emails :as emails])) 180 | 181 | (defp emails/send! #(-> % :file-kind (= :confirmation)) 182 | [email] 183 | (emails/post-email! 184 | (assoc email 185 | :msg (str "Confirmed! It's true " (:name email) ".")))) 186 | ``` 187 | And then you could just call the email namespace from jobs namespace or whatever: 188 | 189 | ```clojure 190 | (ns jobs 191 | (:require [emails :as emails])) 192 | 193 | (def files ; <- dispatching on heterogeneous/inconsistent data 194 | [{:file-kind :confirmation :name "Bob"} 195 | {:file-type :welcome/new :name "Mary"} 196 | {:email-type :promotion :name "Jules"}]) 197 | 198 | (->> files (map emails/send!)) 199 | 200 | ;#_=> :sending-email :msg Confirmed! It's true Bob. 201 | ;#_=> :sending-email :msg Welcome! Glad you're here Mary! 202 | ;#_=> :sending-email :msg Congrats! You got a promotion Jules! 203 | ;#_=> (nil nil nil) 204 | ``` 205 | This gives you the cross-namespace abilities of defmethods with the flexibility of arbitrary predicate dispatch. 206 | 207 | ### Troubleshooting 208 | 209 | Let's go back to our `my-inc` example. 210 | 211 | Imagine we pass in some mysterious data. 212 | ``` clojure 213 | (my-inc "1") 214 | ;#_=> Execution error (ExceptionInfo) at dispacio.core/poly-impl (core.clj:75). 215 | ;No dispatch in polymethod user/eval253$my-inc for arguments: "1" 216 | ``` 217 | We can see the error is thrown by `poly-impl` because the poly `my-inc` has no method for the argument `"1"`. 218 | 219 | Let's give `my-inc` some default behavior so that we can diagnose this anomoly. 220 | ``` clojure 221 | (defp my-inc :poly/default [x] (inc x)) 222 | ;#_=> #object[user$eval253$my-inc__254 0x2b95e48b "user$eval253$my-inc__254@2b95e48b"] 223 | (my-inc "1") 224 | ;#_=> Execution error (ClassCastException) at user/eval268$my-inc>poly-default>x (REPL:1). 225 | ;java.lang.String cannot be cast to java.lang.Number 226 | ``` 227 | Mmmm, we're passing a string to something that expects a number... 228 | 229 | Notice that reference to `user/eval268$my-inc>poly-default>x` attempts to inform us which polymethod threw the error. Specifically, it was the one named `my-inc`, with a predicate of `:poly/default`, translated to `poly-default`, and an argument of `x`. 230 | 231 | With this information, we can tell that the default implementation we just created is passing the error `java.lang.String cannot be cast to java.lang.Number`. 232 | 233 | Let's add a new implementation for strings. 234 | ``` clojure 235 | (defp my-inc string? [x] (inc (read-string x))) 236 | ;#_=> #object[user$eval253$my-inc__254 0x2b95e48b "user$eval253$my-inc__254@2b95e48b"] 237 | (my-inc "1") 238 | ;#_=> 2 239 | ``` 240 | That's better. 241 | 242 | But what about multiple arguments? Just make sure your dispatch function conforms to the manner in which you're passing in arguments. 243 | ``` clojure 244 | (defp my-inc 245 | #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?)) 246 | [x y & z] 247 | (inc (apply + x y z))) 248 | ;#_=> #object[user$eval253$my-inc__254 0x2b95e48b "user$eval253$my-inc__254@2b95e48b"] 249 | (my-inc 1 2 3) 250 | ;#_=> 7 251 | (my-inc 1 2 3 "4") 252 | ;#_=> Execution error (ArityException) at dispacio.core/poly-impl (core.clj:73). 253 | ;Wrong number of args (4) passed to: user/eval268/my-inc>poly-default>x--269 254 | ``` 255 | Because we are not catching strings on more than one argument, the last call took the default path, which we can see takes only one argument, `x`. 256 | 257 | ## Deriving isa? Dispatch 258 | 259 | Similar to multimethods, we can use Clojure's `isa?` hierarchy to resolve arguments. 260 | ``` clojure 261 | (derive java.util.Map ::collection) 262 | ;#_=> nil 263 | (derive java.util.Collection ::collection) 264 | ;#_=> nil 265 | ``` 266 | NOTE: Always put predicate parameters in a vector when you want arguments resolved against `isa?`. 267 | ``` clojure 268 | (defp foo [::collection] [c] :a-collection) 269 | ;#_=> #object[user$eval301$foo__302 0x3f363cf5 "user$eval301$foo__302@3f363cf5"] 270 | (defp foo [String] [s] :a-string) 271 | ;#_=> #object[user$eval301$foo__302 0x3f363cf5 "user$eval301$foo__302@3f363cf5"] 272 | ``` 273 | Ad hoc hierarchies for dispatch a la carte! 274 | ``` clojure 275 | (foo []) 276 | ;#_=> :a-collection 277 | 278 | (foo "bob") 279 | ;#_=> :a-string 280 | ``` 281 | 282 | ## Prefer Dispatch Functions 283 | 284 | As with multimethods, we can prefer some dispatch functions over others. 285 | ``` clojure 286 | (derive ::rect ::shape) 287 | ;#_=> nil 288 | (defp bar [::rect ::shape] [x y] :rect-shape) 289 | ;#_=> #object[user$eval325$bar__326 0x366ef90e "user$eval325$bar__326@366ef90e"] 290 | (defp bar [::shape ::rect] [x y] :shape-rect) 291 | ;#_=> #object[user$eval325$bar__326 0x366ef90e "user$eval325$bar__326@366ef90e"] 292 | (bar ::rect ::rect) 293 | ;#_=> :rect-shape 294 | ``` 295 | We didn't pass in an exact match but, because `::rect` derives from `::shape`, we could match on both. The first dispatch function we find, in the order we made them, that matches the arguments will return its implementation. But what if, for this poly, we wanted the `:shape-rect` implementation? 296 | 297 | To override the default behavior of the hierarchy, use `prefer`. 298 | ``` clojure 299 | (prefer bar [::shape ::rect] [::rect ::shape]) 300 | ;#_=> nil 301 | (bar ::rect ::rect) 302 | ;#_=> :shape-rect 303 | ``` 304 | See the [official docs on multimethods](https://clojure.org/reference/multimethods) for more context. 305 | 306 | ## Spec Validation Dispatch 307 | 308 | Let's try an example from Clojure's docs on spec. 309 | ``` clojure 310 | (require '[clojure.spec.alpha :as s]) 311 | (s/def :animal/kind string?) 312 | (s/def :animal/says string?) 313 | (s/def :animal/common (s/keys :req [:animal/kind :animal/says])) 314 | (s/def :dog/tail? boolean?) 315 | (s/def :dog/breed string?) 316 | (s/def :animal/dog (s/merge :animal/common 317 | (s/keys :req [:dog/tail? :dog/breed]))) 318 | ``` 319 | We can leverage spec hierarchies to do very complex dispatching. 320 | ``` clojure 321 | (defp make-noise (partial s/valid? :animal/dog) 322 | [animal] 323 | (println (-> animal :dog/breed) "barks" (-> animal :animal/says))) 324 | ;#_=> #object[user$eval373$make_noise__374 0x2b491fee "user$eval373$make_noise__374@2b491fee"] 325 | (make-noise 326 | {:animal/kind "dog" 327 | :animal/says "woof" 328 | :dog/tail? true 329 | :dog/breed "retriever"}) 330 | ;#_=> retriever barks woof 331 | ;nil 332 | ``` 333 | ## Function Extension 334 | 335 | In Clojure, we usually extend data types to functions. With polymethods, we can shadow extend functions to arbitrary data types. 336 | 337 | ``` clojure 338 | (defp inc string? [x] (inc (read-string x))) 339 | ;WARNING: inc already refers to: #'clojure.core/inc in namespace: user, being replaced by: #'user/inc 340 | ;#_=> #object[user$eval231$inc__232 0x75ed9710 "user$eval231$inc__232@75ed9710"] 341 | ``` 342 | Here, we are shadowing the `#'clojure.core/inc` function, while storing that original function as the default polymethod implementation. 343 | 344 | ``` clojure 345 | (inc "1") 346 | ;#_=> 2 347 | (inc 1) 348 | ;#_=> 2 349 | ``` 350 | Let's extend `assoc` to associate by index on strings: 351 | 352 | ``` clojure 353 | (defp assoc string? [s i c] (str (subs s 0 i) c (subs s (inc i)))) 354 | ;WARNING: assoc already refers to: cljs.core/assoc being replaced by: cljs.user/assoc at line 1 355 | ;#_=> #object[cljs$user$assoc] 356 | (assoc "abc" 2 'x) 357 | ;#_=> "abx" 358 | ``` 359 | 360 | For now, you can `:exclude` core functions when referring Clojure in order to suppress var replacement warnings. 361 | 362 | ## Choose Your Own Adventure 363 | 364 | You could bring in `core.logic`, `core.match`, Datomic's datalog queries or any other number of inference systems to define your resolution strategy. The world's your oyster. 365 | 366 | ## Bugs 367 | 368 | If you find a bug, submit a 369 | [Github issue](https://github.com/johnmn3/dispacio/issues). 370 | 371 | ## Help 372 | 373 | This project is looking for team members who can help this project succeed! 374 | If you are interested in becoming a team member please open an issue. 375 | 376 | ## License 377 | 378 | Copyright © 2018 John M. Newman III 379 | 380 | Distributed under the MIT License. See [LICENSE](https://github.com/johnmn3/dispacio/blob/master/LICENSE) 381 | --------------------------------------------------------------------------------