Examples - leifp/clj-isa-protocol GitHub Wiki

There will be more examples here later.

Here we extend is-a?, and therefore our multimethod dispatch, to handle maps. The is-a? function is defined in such a way that (is-a? specific-version-of-general-map general-map) is true. This is what you want generally, e.g. (core/isa? specific.Float general.Number) or (core/isa? ::specifically-mantis ::cool-insect)

  (ns foo
    (:use [dispatch.is-a-protocol :only [is-a? Is-A]])
    (:require [dispatch.multimethods :as mm]))

  (defn- map-is-a? [child parent h]
    (and (map? parent)
         (let [sentinel (Object.)]
           (every? 
             (fn [[pk pv]]  ;; for every key in parent
               (let [cv (get child pk sentinel)]
                 (if (identical? cv sentinel)  ;; child has that key
                   false
                   (is-a? h cv pv))))  ;; and their values are related by is-a?
             parent))))
   
  (extend-protocol Is-A
    clojure.lang.IPersistentMap
    (-is-a? [c p h] (map-is-a? c p h)))

This now makes dispatching, say, Ring requests to more and more specific handlers easy, and easy to extend:

  (mm/defmulti handler "multimethod ring handler" (fn [req] req))
  (mm/defmethod handler {}
    [req] {:status 404 :headers {} :body "Not found."})
  (mm/defmethod handler {:request-method :get}
    [req] {:status 200 :headers {} :body "Generic GET."})
  (mm/defmethod handler {:request-method :get, :content-type "application/json"}
    [req] {:status 200 :headers {} :body "\"GET some json.\""})
  (mm/defmethod handler {:request-method :get, :content-type "application/xml"}
    [req] {:status 200 :headers {} :body "<foo>GET some XML.</foo>"})
  (mm/defmethod handler {:request-method :get,
                         :content-type "application/json"
                         :headers {"app/client-status" "favored-nation"}}
    [req] {:status 200 :headers {} :body "\"GET some super-special json.\""})
   
  (handler {:request-method :get})
  ;=> {:status 200, :headers {}, :body "Generic GET."}
  (handler {:request-method :post})
  ;=> {:status 404, :headers {}, :body "Not found."}
  (handler {:request-method :get :content-type "application/xml"})
  ;=> {:status 200, :headers {}, :body "<foo>GET some XML.</foo>"}
  (handler {:request-method :get :content-type "application/json"})
  ;=> {:status 200, :headers {}, :body "\"GET some json.\""}
  (handler {:request-method :get :headers {"anything" "really"} :content-type "application/json"})
  ;=> {:status 200, :headers {}, :body "\"GET some json.\""}
  (handler {:request-method :get :headers {"anything" "really", "app/client-status" "favored-nation"} :content-type "application/json"})
  ;=> {:status 200, :headers {}, :body "\"GET some super-special json.\""}

Notice that while you could have done something similar with clojure.core's multimethods, it's not quite as flexible:

  (core/defmulti handler "multimethod ring handler" (juxt :request-method :content-type))
  (core/defmethod handler [:get "application/json"] ...)
  ;; Oh, wait, now I want to do special things depending on the headers...
  ;; But I can't, unless I change the implementation of the above method.
  
  ;; Or, less likely, change the dispatch fn and value for all methods:
  (core/defmulti handler "multimethod ring handler"
    (juxt :request-method :content-type #(get-in % [:headers "special"])))
  (core/defmethod handler [:get "application/json" nil] ...)
  ;; and the same for lots of methods... 

This is similar to the way that document DBs are more flexible than relational DBs about the data model.

⚠️ **GitHub.com Fallback** ⚠️