Successfully reported this slideshow.
Dynamic Polymorphism                         in Clojure                                -or-                       How I le...
Outline                    • Intro - a bit about me                    • The problem                    • Approaches      ...
About Me                    • Reformed academic                     • wrote 1000’s of lines of bad Lisp code              ...
The ProblemFriday, 11 May 12
The Problem                                                                                           discogs            (...
Controller             (ns record-nav.core               (:use [compojure.core :only (defroutes GET POST)])               ...
model.clj                    (defn get-releases-for [query]                      (do-some-munging                        (...
So why not keep the                     same function name                    and dynamically switch                      ...
with namespace binding       config.clj             {:online {:datasource record-nav.discogs}              :offline {:datas...
the macro     (defmacro with-namespace-binding       "Takes a single [namespace-keyword function] pair"       [[namespace-...
Summary                    •   Not very intrusive                        •   Just wrap the function call in a             ...
Maybe we should be                    using protocols instead?Friday, 11 May 12
Protocols                    • Abstraction and contracts without the OO                      concept of inheritance       ...
config.clj (trees of datatypes)       {:online [record-nav.model [record-nav.discogs]]        :stubs [record-nav.model [rec...
model.clj(defprotocol Model  (releases-for [this query])  (release-by-id [this release-id])  (get-players-for [this releas...
core.clj (the controller)   (let [the-model (loom/get-wired nil)]     (defroutes record-nav-routes       (GET "/" [] (layo...
Testing(defrecord TestArtistData []  data-source/DataSource  (search [this query] nil)  (get-release [this release-id]    ...
Summary                    • Heading toward a typical OO interface-                      driven dependency-injection frame...
OK, I suppose I should                     try out multimethods                    since it is the “official”              ...
multimethods                    • Use defmulti to declare a method name                      and a function to be used for...
data_source.clj (the abstraction)      (defn app-env [& args]        (keyword (get (System/getenv) "APP_ENV")))      (defm...
Testing  (defmethod data-source/search :test [query] sample-release-data)  (fact    (model/releases-for anything) => (has ...
Summary                • Least code of all the solutions by far                • True runtime polymorphism                ...
The EndFriday, 11 May 12
Upcoming SlideShare
Loading in …5
×

Dynamic poly-preso

452 views

Published on

Presentation on various (sometimes weird) approaches to dynamic polymophism in Clojure that I presented at the clj-melb user group.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Dynamic poly-preso

  1. 1. Dynamic Polymorphism in Clojure -or- How I learned to stop configuring and start loving multimethods Scott Shaw <scottwshaw@gmail.com>Friday, 11 May 12
  2. 2. Outline • Intro - a bit about me • The problem • Approaches • Late namespace binding • Protocols • MultimethodsFriday, 11 May 12
  3. 3. About Me • Reformed academic • wrote 1000’s of lines of bad Lisp code back in the day • Now, IT consultant • developer, architect, manager, technologist, consultant for ThoughtWorks in AsiaPacFriday, 11 May 12
  4. 4. The ProblemFriday, 11 May 12
  5. 5. The Problem discogs (POST "/search" (search query) [query] Controller (releases-for query) model (search query) ? stubs Switchable at runtime Nonintrusive Testable GenericFriday, 11 May 12
  6. 6. Controller (ns record-nav.core (:use [compojure.core :only (defroutes GET POST)]) (:require [compojure.route :as route] [compojure.handler :as handler] [clojure.contrib.json :as json] [record-nav.model :as model] [record-nav.views.layout :as layout] [record-nav.views.search :as search-view])) (defroutes record-nav-routes (GET "/" [] (layout/common "Record Nav" (search-view/form ""))) (POST "/search" [query] (let [res (model/get-releases-for query)] (search-view/results res query))) (route/resources "/") (route/not-found (layout/four-oh-four))) (def app (handler/site record-nav-routes))Friday, 11 May 12
  7. 7. model.clj (defn get-releases-for [query] (do-some-munging (discogs/search query))) - or - (defn get-releases-for [query] (do-some-munging (stub-data/search query)))Friday, 11 May 12
  8. 8. So why not keep the same function name and dynamically switch the namespace depending on the environment?Friday, 11 May 12
  9. 9. with namespace binding config.clj {:online {:datasource record-nav.discogs} :offline {:datasource record-nav.stub-data}} model.clj (defn get-releases-for [query] (nsb/with-namespace-bindings [:datasource search] (search query))) discogs.clj (defn search [query] (let [response (execute-query query)] (-> (json/read-json response) :resp :search :searchresults :results))) stub-data.clj (defn search [query] sample-data)Friday, 11 May 12
  10. 10. the macro (defmacro with-namespace-binding "Takes a single [namespace-keyword function] pair" [[namespace-id function] & body] (let [the-namespace (namespace-id (namespace-bindings))] `(do (when-not (find-ns ~the-namespace) (require ~the-namespace)) (let [~function (ns-resolve ~the-namespace ~function)] ~@body))))Friday, 11 May 12
  11. 11. Summary • Not very intrusive • Just wrap the function call in a (let [:namespace function] ...) form • Don’t have to anticipate the potential namespaces in the calling function (no explicit require) • Testing isn’t great • tests are run after macro expansion so difficult to inject behaviour without configuration.Friday, 11 May 12
  12. 12. Maybe we should be using protocols instead?Friday, 11 May 12
  13. 13. Protocols • Abstraction and contracts without the OO concept of inheritance • Define protocols for each participant, Model and DataSource. • In a configuration file, define a wiring pattern of datatypes for each environmental option • You end up with a typical OO dependency injection framework (with an implicit constructor function)Friday, 11 May 12
  14. 14. config.clj (trees of datatypes) {:online [record-nav.model [record-nav.discogs]] :stubs [record-nav.model [record-nav.stub-data]]} data_source.clj (defprotocol DataSource (search [this query]) (get-release [this release-id])) discogs.clj (defn create [] (reify data-source/DataSource (search [this query] (let [response (client/get (str "http://api.discogs.com/search?q=" (codec/url-encode query)) {:as :json})] (-> response :body :resp :search :searchresults :results))) (get-release [this release-id] (let [response (client/get (str "http://api.discogs.com/releases" release-id) {:as :json})] (-> response :resp :release)))))Friday, 11 May 12
  15. 15. model.clj(defprotocol Model (releases-for [this query]) (release-by-id [this release-id]) (get-players-for [this release-id]))(defn create "Creates a model object with implementations that use the data-connector argument" [data-connector] (reify Model (releases-for [this query] (data-source/search data-connector query)) (release-by-id [this release-id] (data-source/get-release data-connector release-id)) (get-players-for [this release-id] (let [release (release-by-id this release-id) artist-uris (map :resource_url (:artists release))] (mapcat retrieve-artists artist-uris))))) stub_data.clj (defn create [] (reify data-source/DataSource (search [this query] sample-response) (get-release [this release-id] release-data)))Friday, 11 May 12
  16. 16. core.clj (the controller) (let [the-model (loom/get-wired nil)] (defroutes record-nav-routes (GET "/" [] (layout/common "Record Nav" (search-view/form ""))) (POST "/search" [query] (let [res (model/releases-for the-model query)] (search-view/results res query))) (route/resources "/") (route/not-found (layout/four-oh-four)))) loom.clj (the macro) (defmacro get-wired [pattern-key] (let [[ns-1 [ns-2]] (get-wiring-pattern pattern-key)] `(do (when-not (find-ns ~ns-1) (require ~ns-1)) (when-not (find-ns ~ns-2) (require ~ns-2)) (let [c1# (ns-resolve ~ns-1 ~create) c2# (ns-resolve ~ns-2 ~create)] (c1# (c2#))))))Friday, 11 May 12
  17. 17. Testing(defrecord TestArtistData [] data-source/DataSource (search [this query] nil) (get-release [this release-id] {:artists [{:resource_url "testurl-1"} {:resource_url "testurl-2"}]}))(let [model-under-test (model/create (TestArtistData.))] (fact "for a release with multiple artists, should concatenate all artists info" (get-players-for model-under-test anything) => (contains *ron-wood-guitar* *charlie-watts* :in-any-order :gaps-ok) (provided (model/retrieve-artists "testurl-1") => [*ron-wood-guitar* *ron-wood-bass*] (retrieve-artists "testurl-2") => [*charlie-watts*])))Friday, 11 May 12
  18. 18. Summary • Heading toward a typical OO interface- driven dependency-injection framework • Am I rewriting Spring in Clojure? • Fairly intrusive in that it requires a protocol to be defined for each participant • Nice ability to inject behaviour in tests • Still hard to test the framework itselfFriday, 11 May 12
  19. 19. OK, I suppose I should try out multimethods since it is the “official” Clojure approach to runtime polymorphismFriday, 11 May 12
  20. 20. multimethods • Use defmulti to declare a method name and a function to be used for dispatch • Use defmethod to define functions for each possible dispatch value • In our case... • define a generic set of multimethods that a data-source must implement • create new data sources by defmethods • the target environment is encoded in the method implementationFriday, 11 May 12
  21. 21. data_source.clj (the abstraction) (defn app-env [& args] (keyword (get (System/getenv) "APP_ENV"))) (defmulti search #app-env :default :online) (defmulti get-release #app-env :default :online) stub_data.clj (defmethod data-source/search :stubs [query] sample-response) (defmethod data-source/get-release :stubs [release-id] release-data) discogs.clj (defmethod data-source/search :online [query] (let [response (client/get (str "http://api.discogs.com/search?q=" (codec/url-encode query)) {:as :json})] (-> response :body :resp :search :searchresults :results))) (defmethod data-source/get-release :online [release-id] (let [response (client/get (str "http://api.discogs.com/releases" release-id) {:as :json})] (-> response :resp :release)))Friday, 11 May 12
  22. 22. Testing (defmethod data-source/search :test [query] sample-release-data) (fact (model/releases-for anything) => (has every? contains-title-and-uri-strings?) (provided (data-source/app-env anything) => :test))Friday, 11 May 12
  23. 23. Summary • Least code of all the solutions by far • True runtime polymorphism • No ugly macros • Easy to inject behaviour for tests • No configuration! • Model must be aware of implementations to a certain extent • (require ‘[all possible namespaces that might be dispatched])Friday, 11 May 12
  24. 24. The EndFriday, 11 May 12

×