Dispatching function calls on different formats of maps












2














I'm writing an agar.io clone. I've lately seen a lot of suggestions to limit use of records (like here), so I'm trying to do the whole project only using basic maps.*



I ended up creating constructors for different "types" of bacteria like



(defn new-bacterium [starting-position]
{:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))


The "directed bacterium" has a new entry added to it. The :direction entry will be used to remember what direction it was heading in.



Here's the problem: I want to have one function take-turn that accepts the bacterium and the current state of the world, and returns a vector of [x, y] indicating the offset from the current position to move the bacterium to. I want to have a single function that's called because I can think right now of at least three kinds of bacteria that I'll want to have, and would like to have the ability to add new types later that each define their own take-turn.



A Can-Take-Turn protocol is out the window since I'm just using plain maps.



A take-turn multimethod seemed like it would work at first, but then I realized that I'd have no dispatch values to use in my current setup that would be extensible. I could have :direction be the dispatch function, and then dispatch on nil to use the "directed bacterium"'s take-turn, or default to get the base aimless behavior, but that doesn't give me a way of even having a third "player bacterium" type.



The only solution I can think of it to require that all bacterium have a :type field, and to dispatch on it, like:



(defn new-bacterium [starting-position]
{:type :aimless
:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :type :directed,
:direction starting-directions)))

(defmulti take-turn (fn [b _] (:type b)))

(defmethod take-turn :aimless [this world]
(println "Aimless turn!"))

(defmethod take-turn :directed [this world]
(println "Directed turn!"))

(take-turn (new-bacterium [0 0]) nil)
Aimless turn!
=> nil

(take-turn (new-directed-bacterium [0 0] nil) nil)
Directed turn!
=> nil


But now I'm back to basically dispatching on type, using a slower method than protocols. Is this a legitimate case to use records and protocols, or is there something about mutlimethods that I'm missing? I don't have a lot of practice with them.





* I also decided to try this because I was in the situation where I had a Bacterium record and wanted to create a new "directed" version of the record that had a single field direction added to it (inheritance basically). The original record implemented protocols though, and I didn't want to have to do something like nesting the original record in the new one, and routing all behavior to the nested instance. Every time I created a new type or changed a protocol, I would have to change all the routing, which was a lot of work.










share|improve this question


















  • 1




    I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
    – Carcigenicate
    Nov 16 at 2:10


















2














I'm writing an agar.io clone. I've lately seen a lot of suggestions to limit use of records (like here), so I'm trying to do the whole project only using basic maps.*



I ended up creating constructors for different "types" of bacteria like



(defn new-bacterium [starting-position]
{:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))


The "directed bacterium" has a new entry added to it. The :direction entry will be used to remember what direction it was heading in.



Here's the problem: I want to have one function take-turn that accepts the bacterium and the current state of the world, and returns a vector of [x, y] indicating the offset from the current position to move the bacterium to. I want to have a single function that's called because I can think right now of at least three kinds of bacteria that I'll want to have, and would like to have the ability to add new types later that each define their own take-turn.



A Can-Take-Turn protocol is out the window since I'm just using plain maps.



A take-turn multimethod seemed like it would work at first, but then I realized that I'd have no dispatch values to use in my current setup that would be extensible. I could have :direction be the dispatch function, and then dispatch on nil to use the "directed bacterium"'s take-turn, or default to get the base aimless behavior, but that doesn't give me a way of even having a third "player bacterium" type.



The only solution I can think of it to require that all bacterium have a :type field, and to dispatch on it, like:



(defn new-bacterium [starting-position]
{:type :aimless
:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :type :directed,
:direction starting-directions)))

(defmulti take-turn (fn [b _] (:type b)))

(defmethod take-turn :aimless [this world]
(println "Aimless turn!"))

(defmethod take-turn :directed [this world]
(println "Directed turn!"))

(take-turn (new-bacterium [0 0]) nil)
Aimless turn!
=> nil

(take-turn (new-directed-bacterium [0 0] nil) nil)
Directed turn!
=> nil


But now I'm back to basically dispatching on type, using a slower method than protocols. Is this a legitimate case to use records and protocols, or is there something about mutlimethods that I'm missing? I don't have a lot of practice with them.





* I also decided to try this because I was in the situation where I had a Bacterium record and wanted to create a new "directed" version of the record that had a single field direction added to it (inheritance basically). The original record implemented protocols though, and I didn't want to have to do something like nesting the original record in the new one, and routing all behavior to the nested instance. Every time I created a new type or changed a protocol, I would have to change all the routing, which was a lot of work.










share|improve this question


















  • 1




    I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
    – Carcigenicate
    Nov 16 at 2:10
















2












2








2







I'm writing an agar.io clone. I've lately seen a lot of suggestions to limit use of records (like here), so I'm trying to do the whole project only using basic maps.*



I ended up creating constructors for different "types" of bacteria like



(defn new-bacterium [starting-position]
{:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))


The "directed bacterium" has a new entry added to it. The :direction entry will be used to remember what direction it was heading in.



Here's the problem: I want to have one function take-turn that accepts the bacterium and the current state of the world, and returns a vector of [x, y] indicating the offset from the current position to move the bacterium to. I want to have a single function that's called because I can think right now of at least three kinds of bacteria that I'll want to have, and would like to have the ability to add new types later that each define their own take-turn.



A Can-Take-Turn protocol is out the window since I'm just using plain maps.



A take-turn multimethod seemed like it would work at first, but then I realized that I'd have no dispatch values to use in my current setup that would be extensible. I could have :direction be the dispatch function, and then dispatch on nil to use the "directed bacterium"'s take-turn, or default to get the base aimless behavior, but that doesn't give me a way of even having a third "player bacterium" type.



The only solution I can think of it to require that all bacterium have a :type field, and to dispatch on it, like:



(defn new-bacterium [starting-position]
{:type :aimless
:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :type :directed,
:direction starting-directions)))

(defmulti take-turn (fn [b _] (:type b)))

(defmethod take-turn :aimless [this world]
(println "Aimless turn!"))

(defmethod take-turn :directed [this world]
(println "Directed turn!"))

(take-turn (new-bacterium [0 0]) nil)
Aimless turn!
=> nil

(take-turn (new-directed-bacterium [0 0] nil) nil)
Directed turn!
=> nil


But now I'm back to basically dispatching on type, using a slower method than protocols. Is this a legitimate case to use records and protocols, or is there something about mutlimethods that I'm missing? I don't have a lot of practice with them.





* I also decided to try this because I was in the situation where I had a Bacterium record and wanted to create a new "directed" version of the record that had a single field direction added to it (inheritance basically). The original record implemented protocols though, and I didn't want to have to do something like nesting the original record in the new one, and routing all behavior to the nested instance. Every time I created a new type or changed a protocol, I would have to change all the routing, which was a lot of work.










share|improve this question













I'm writing an agar.io clone. I've lately seen a lot of suggestions to limit use of records (like here), so I'm trying to do the whole project only using basic maps.*



I ended up creating constructors for different "types" of bacteria like



(defn new-bacterium [starting-position]
{:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))


The "directed bacterium" has a new entry added to it. The :direction entry will be used to remember what direction it was heading in.



Here's the problem: I want to have one function take-turn that accepts the bacterium and the current state of the world, and returns a vector of [x, y] indicating the offset from the current position to move the bacterium to. I want to have a single function that's called because I can think right now of at least three kinds of bacteria that I'll want to have, and would like to have the ability to add new types later that each define their own take-turn.



A Can-Take-Turn protocol is out the window since I'm just using plain maps.



A take-turn multimethod seemed like it would work at first, but then I realized that I'd have no dispatch values to use in my current setup that would be extensible. I could have :direction be the dispatch function, and then dispatch on nil to use the "directed bacterium"'s take-turn, or default to get the base aimless behavior, but that doesn't give me a way of even having a third "player bacterium" type.



The only solution I can think of it to require that all bacterium have a :type field, and to dispatch on it, like:



(defn new-bacterium [starting-position]
{:type :aimless
:mass 0,
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :type :directed,
:direction starting-directions)))

(defmulti take-turn (fn [b _] (:type b)))

(defmethod take-turn :aimless [this world]
(println "Aimless turn!"))

(defmethod take-turn :directed [this world]
(println "Directed turn!"))

(take-turn (new-bacterium [0 0]) nil)
Aimless turn!
=> nil

(take-turn (new-directed-bacterium [0 0] nil) nil)
Directed turn!
=> nil


But now I'm back to basically dispatching on type, using a slower method than protocols. Is this a legitimate case to use records and protocols, or is there something about mutlimethods that I'm missing? I don't have a lot of practice with them.





* I also decided to try this because I was in the situation where I had a Bacterium record and wanted to create a new "directed" version of the record that had a single field direction added to it (inheritance basically). The original record implemented protocols though, and I didn't want to have to do something like nesting the original record in the new one, and routing all behavior to the nested instance. Every time I created a new type or changed a protocol, I would have to change all the routing, which was a lot of work.







clojure multimethod






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked Nov 16 at 0:23









Carcigenicate

17.2k43058




17.2k43058








  • 1




    I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
    – Carcigenicate
    Nov 16 at 2:10
















  • 1




    I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
    – Carcigenicate
    Nov 16 at 2:10










1




1




I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
– Carcigenicate
Nov 16 at 2:10






I ended up going with the "dispatch on :type" idea for the time being, and it seems to be going well. I still don't know if it's best practice, but I think it will work fine.
– Carcigenicate
Nov 16 at 2:10














3 Answers
3






active

oldest

votes


















2














You can use example-based multiple dispatch for this, as explained in this blog post. It is certainly not the most performant way to solve this problem, but arguably more flexible than multi-methods as it does not require you to declare a dispatch-method upfront. So it is open for extension to any data representation, even other things than maps. If you need performance, then multi-methods or protocols as you suggest, is probably the way to go.



First, you need to add a dependency on [bluebell/utils "1.5.0"] and require [bluebell.utils.ebmd :as ebmd]. Then you declare constructors for your data structures (copied from your question) and functions to test those data strucutres:



(defn new-bacterium [starting-position]
{:mass 0
:position starting-position})

(defn new-directed-bacterium [starting-position starting-directions]
(-> (new-bacterium starting-position)
(assoc :direction starting-directions)))

(defn bacterium? [x]
(and (map? x)
(contains? x :position)))

(defn directed-bacterium? [x]
(and (bacterium? x)
(contains? x :direction)))


Now we are going to register those datastructures as so called arg-specs so that we can use them for dispatch:



(ebmd/def-arg-spec ::bacterium {:pred bacterium?
:pos [(new-bacterium [9 8])]
:neg [3 4]})

(ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
:pos [(new-directed-bacterium [9 8] [3 4])]
:neg [(new-bacterium [3 4])]})


For each arg-spec, we need to declare a few example values under the :pos key, and a few non-examples under the :neg key. Those values are used to resolve the fact that a directed-bacterium is more specific than just a bacterium in order for the dispatch to work properly.



Finally, we are going to define a polymorphic take-turn function. We first declare it, using declare-poly:



(ebmd/declare-poly take-turn)


And then, we can provide different implementations for specific arguments:



(ebmd/def-poly take-turn [::bacterium x
::ebmd/any-arg world]
:aimless)

(ebmd/def-poly take-turn [::directed-bacterium x
::ebmd/any-arg world]
:directed)


Here, the ::ebmd/any-arg is an arg-spec that matches any argument. The above approach is open to extension just like multi-methods, but does not require you to declare a :type field upfront and is thus more flexible. But, as I said, it is also going to be slower than both multimethods and protocols, so ultimately this is a trade-off.



Here is the full solution: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj






share|improve this answer





























    2














    Dispatching a multimethod by a :type field is indeed polymorphic dispatch that could be done with a protocol, but using multimethods allows you to dispatch on different fields. You can add a second multimethod that dispatches on something other than :type, which might be tricky to accomplish with a protocol (or even multiple protocols).



    Since a multimethod can dispatch on anything, you could use a set as the dispatch value. Here's an alternative approach. It's not fully extensible, since the keys to select are determined within the dispatch function, but it might give you an idea for a better solution:



    (defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

    (defmethod take-turn #{} [this world]
    (println "Aimless turn!"))

    (defmethod take-turn #{:direction} [this world]
    (println "Directed turn!"))





    share|improve this answer





























      1














      Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.



      First, we'll start off with an atom to store all of our polymorphic functions:



      (def polies (atom {}))


      In usage, the internal structure of the polies would look something like this:



      {foo ; <- function name
      {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
      [pred1 fn1 1 ()]
      [pred2 fn2 2 '&]]
      :prefer {:this-pred #{:that-pred :other-pred}}}
      bar
      {:dispatch [[pred0 fn0 1 ()]
      [pred1 fn1 3 ()]]
      :prefer {:some-pred #{:any-pred}}}}


      Now, let's make it so that we can prefer predicates (like prefer-method):



      (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

      (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
      (if-let [p (-> @polies (get-in [poly :prefer v1]))]
      (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

      (defn- default-sort [v1 v2]
      (if (= v1 :poly/default)
      1
      (if (= v2 :poly/default)
      -1
      0)))

      (defn- pref [poly v1 v2]
      (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
      -1
      (default-sort v1 v2)))

      (defn- sort-disp [poly]
      (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

      (defn prefer [poly v1 v2]
      (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
      (sort-disp poly)
      nil)


      Now, let's create our dispatch lookup system:



      (defn- get-disp [poly filter-fn]
      (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

      (defn- pred->disp [poly pred]
      (get-disp poly #(-> % first (= pred))))

      (defn- pred->poly-fn [poly pred]
      (-> poly (pred->disp pred) second))

      (defn- check-args-length [disp args]
      ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

      (defn- args-are? [disp args]
      (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

      (defn- check-dispatch-on-args [disp args]
      (if (-> disp first vector?)
      (-> disp (args-are? args))
      (-> disp first (apply args))))

      (defn- disp*args? [disp args]
      (and (check-args-length disp args)
      (check-dispatch-on-args disp args)))

      (defn- args->poly-fn [poly args]
      (-> poly (get-disp #(disp*args? % args)) second))


      Next, let's prepare our define macro with some initialization and setup functions:



      (defn- poly-impl [poly args]
      (if-let [poly-fn (-> poly (args->poly-fn args))]
      (-> poly-fn (apply args))
      (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
      (-> default-poly-fn (apply args))
      (throw (ex-info (str "No poly for " poly " with " args) {})))))

      (defn- remove-disp [poly pred]
      (when-let [disp (pred->disp poly pred)]
      (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

      (defn- til& [args]
      (count (take-while (partial not= '&) args)))

      (defn- add-disp [poly poly-fn pred params]
      (swap! polies update-in [poly :dispatch]
      #(-> % (or ) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

      (defn- setup-poly [poly poly-fn pred params]
      (remove-disp poly pred)
      (add-disp poly poly-fn pred params)
      (sort-disp poly))


      With that, we can finally build our polies by rubbing some macro juice on there:



      (defmacro defpoly [poly-name pred params body]
      `(do (when-not (-> ~poly-name quote resolve bound?)
      (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
      (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
      (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
      ~poly-name))


      Now you can build arbitrary predicate dispatch:



      ;; use defpoly like defmethod, but without a defmulti declaration
      ;; unlike defmethods, all params are passed to defpoly's predicate function
      (defpoly myinc number? [x] (inc x))

      (myinc 1)
      ;#_=> 2

      (myinc "1")
      ;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
      ;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

      (defpoly myinc :poly/default [x] (inc x))

      (myinc "1")
      ;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
      ;java.lang.String cannot be cast to java.lang.Number

      (defpoly myinc string? [x] (inc (read-string x)))

      (myinc "1")
      ;#_=> 2

      (defpoly myinc
      #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
      [x y & z]
      (inc (apply + x y z)))

      (myinc 1 2 3)
      ;#_=> 7

      (myinc 1 2 3 "4")
      ;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
      ;Wrong number of args (4) passed to: user/eval523/fn--524

      ; ^ took the :poly/default path


      And when using your example, we can see:



      (defn new-bacterium [starting-position]
      {:mass 0,
      :position starting-position})

      (defn new-directed-bacterium [starting-position starting-directions]
      (-> (new-bacterium starting-position)
      (assoc :direction starting-directions)))

      (defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
      [this world]
      (println "Directed turn!"))

      ;; or, if you'd rather use spec
      (defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
      [this world]
      (println "Directed turn!"))

      (take-turn (new-directed-bacterium [0 0] nil) nil)
      ;#_=> Directed turn!
      ;nil

      (defpoly take-turn :poly/default [this world]
      (println "Aimless turn!"))

      (take-turn (new-bacterium [0 0]) nil)
      ;#_=> Aimless turn!
      ;nil

      (defpoly take-turn #(-> %& first :show) [this world]
      (println :this this :world world))

      (take-turn (assoc (new-bacterium [0 0]) :show true) nil)
      ;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
      ;nil


      Now, let's try using isa? relationships, a la defmulti:



      (derive java.util.Map ::collection)

      (derive java.util.Collection ::collection)

      ;; always wrap classes in a vector to dispatch off of isa? relationships
      (defpoly foo [::collection] [c] :a-collection)

      (defpoly foo [String] [s] :a-string)

      (foo )
      ;#_=> :a-collection

      (foo "bob")
      ;#_=> :a-string


      And of course we can use prefer to disambiguate relationships:



      (derive ::rect ::shape)

      (defpoly bar [::rect ::shape] [x y] :rect-shape)

      (defpoly bar [::shape ::rect] [x y] :shape-rect)

      (bar ::rect ::rect)
      ;#_=> :rect-shape

      (prefer bar [::shape ::rect] [::rect ::shape])

      (bar ::rect ::rect)
      ;#_=> :shape-rect


      Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.






      share|improve this answer























        Your Answer






        StackExchange.ifUsing("editor", function () {
        StackExchange.using("externalEditor", function () {
        StackExchange.using("snippets", function () {
        StackExchange.snippets.init();
        });
        });
        }, "code-snippets");

        StackExchange.ready(function() {
        var channelOptions = {
        tags: "".split(" "),
        id: "1"
        };
        initTagRenderer("".split(" "), "".split(" "), channelOptions);

        StackExchange.using("externalEditor", function() {
        // Have to fire editor after snippets, if snippets enabled
        if (StackExchange.settings.snippets.snippetsEnabled) {
        StackExchange.using("snippets", function() {
        createEditor();
        });
        }
        else {
        createEditor();
        }
        });

        function createEditor() {
        StackExchange.prepareEditor({
        heartbeatType: 'answer',
        autoActivateHeartbeat: false,
        convertImagesToLinks: true,
        noModals: true,
        showLowRepImageUploadWarning: true,
        reputationToPostImages: 10,
        bindNavPrevention: true,
        postfix: "",
        imageUploader: {
        brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
        contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
        allowUrls: true
        },
        onDemand: true,
        discardSelector: ".discard-answer"
        ,immediatelyShowMarkdownHelp:true
        });


        }
        });














        draft saved

        draft discarded


















        StackExchange.ready(
        function () {
        StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53329709%2fdispatching-function-calls-on-different-formats-of-maps%23new-answer', 'question_page');
        }
        );

        Post as a guest















        Required, but never shown

























        3 Answers
        3






        active

        oldest

        votes








        3 Answers
        3






        active

        oldest

        votes









        active

        oldest

        votes






        active

        oldest

        votes









        2














        You can use example-based multiple dispatch for this, as explained in this blog post. It is certainly not the most performant way to solve this problem, but arguably more flexible than multi-methods as it does not require you to declare a dispatch-method upfront. So it is open for extension to any data representation, even other things than maps. If you need performance, then multi-methods or protocols as you suggest, is probably the way to go.



        First, you need to add a dependency on [bluebell/utils "1.5.0"] and require [bluebell.utils.ebmd :as ebmd]. Then you declare constructors for your data structures (copied from your question) and functions to test those data strucutres:



        (defn new-bacterium [starting-position]
        {:mass 0
        :position starting-position})

        (defn new-directed-bacterium [starting-position starting-directions]
        (-> (new-bacterium starting-position)
        (assoc :direction starting-directions)))

        (defn bacterium? [x]
        (and (map? x)
        (contains? x :position)))

        (defn directed-bacterium? [x]
        (and (bacterium? x)
        (contains? x :direction)))


        Now we are going to register those datastructures as so called arg-specs so that we can use them for dispatch:



        (ebmd/def-arg-spec ::bacterium {:pred bacterium?
        :pos [(new-bacterium [9 8])]
        :neg [3 4]})

        (ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
        :pos [(new-directed-bacterium [9 8] [3 4])]
        :neg [(new-bacterium [3 4])]})


        For each arg-spec, we need to declare a few example values under the :pos key, and a few non-examples under the :neg key. Those values are used to resolve the fact that a directed-bacterium is more specific than just a bacterium in order for the dispatch to work properly.



        Finally, we are going to define a polymorphic take-turn function. We first declare it, using declare-poly:



        (ebmd/declare-poly take-turn)


        And then, we can provide different implementations for specific arguments:



        (ebmd/def-poly take-turn [::bacterium x
        ::ebmd/any-arg world]
        :aimless)

        (ebmd/def-poly take-turn [::directed-bacterium x
        ::ebmd/any-arg world]
        :directed)


        Here, the ::ebmd/any-arg is an arg-spec that matches any argument. The above approach is open to extension just like multi-methods, but does not require you to declare a :type field upfront and is thus more flexible. But, as I said, it is also going to be slower than both multimethods and protocols, so ultimately this is a trade-off.



        Here is the full solution: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj






        share|improve this answer


























          2














          You can use example-based multiple dispatch for this, as explained in this blog post. It is certainly not the most performant way to solve this problem, but arguably more flexible than multi-methods as it does not require you to declare a dispatch-method upfront. So it is open for extension to any data representation, even other things than maps. If you need performance, then multi-methods or protocols as you suggest, is probably the way to go.



          First, you need to add a dependency on [bluebell/utils "1.5.0"] and require [bluebell.utils.ebmd :as ebmd]. Then you declare constructors for your data structures (copied from your question) and functions to test those data strucutres:



          (defn new-bacterium [starting-position]
          {:mass 0
          :position starting-position})

          (defn new-directed-bacterium [starting-position starting-directions]
          (-> (new-bacterium starting-position)
          (assoc :direction starting-directions)))

          (defn bacterium? [x]
          (and (map? x)
          (contains? x :position)))

          (defn directed-bacterium? [x]
          (and (bacterium? x)
          (contains? x :direction)))


          Now we are going to register those datastructures as so called arg-specs so that we can use them for dispatch:



          (ebmd/def-arg-spec ::bacterium {:pred bacterium?
          :pos [(new-bacterium [9 8])]
          :neg [3 4]})

          (ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
          :pos [(new-directed-bacterium [9 8] [3 4])]
          :neg [(new-bacterium [3 4])]})


          For each arg-spec, we need to declare a few example values under the :pos key, and a few non-examples under the :neg key. Those values are used to resolve the fact that a directed-bacterium is more specific than just a bacterium in order for the dispatch to work properly.



          Finally, we are going to define a polymorphic take-turn function. We first declare it, using declare-poly:



          (ebmd/declare-poly take-turn)


          And then, we can provide different implementations for specific arguments:



          (ebmd/def-poly take-turn [::bacterium x
          ::ebmd/any-arg world]
          :aimless)

          (ebmd/def-poly take-turn [::directed-bacterium x
          ::ebmd/any-arg world]
          :directed)


          Here, the ::ebmd/any-arg is an arg-spec that matches any argument. The above approach is open to extension just like multi-methods, but does not require you to declare a :type field upfront and is thus more flexible. But, as I said, it is also going to be slower than both multimethods and protocols, so ultimately this is a trade-off.



          Here is the full solution: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj






          share|improve this answer
























            2












            2








            2






            You can use example-based multiple dispatch for this, as explained in this blog post. It is certainly not the most performant way to solve this problem, but arguably more flexible than multi-methods as it does not require you to declare a dispatch-method upfront. So it is open for extension to any data representation, even other things than maps. If you need performance, then multi-methods or protocols as you suggest, is probably the way to go.



            First, you need to add a dependency on [bluebell/utils "1.5.0"] and require [bluebell.utils.ebmd :as ebmd]. Then you declare constructors for your data structures (copied from your question) and functions to test those data strucutres:



            (defn new-bacterium [starting-position]
            {:mass 0
            :position starting-position})

            (defn new-directed-bacterium [starting-position starting-directions]
            (-> (new-bacterium starting-position)
            (assoc :direction starting-directions)))

            (defn bacterium? [x]
            (and (map? x)
            (contains? x :position)))

            (defn directed-bacterium? [x]
            (and (bacterium? x)
            (contains? x :direction)))


            Now we are going to register those datastructures as so called arg-specs so that we can use them for dispatch:



            (ebmd/def-arg-spec ::bacterium {:pred bacterium?
            :pos [(new-bacterium [9 8])]
            :neg [3 4]})

            (ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
            :pos [(new-directed-bacterium [9 8] [3 4])]
            :neg [(new-bacterium [3 4])]})


            For each arg-spec, we need to declare a few example values under the :pos key, and a few non-examples under the :neg key. Those values are used to resolve the fact that a directed-bacterium is more specific than just a bacterium in order for the dispatch to work properly.



            Finally, we are going to define a polymorphic take-turn function. We first declare it, using declare-poly:



            (ebmd/declare-poly take-turn)


            And then, we can provide different implementations for specific arguments:



            (ebmd/def-poly take-turn [::bacterium x
            ::ebmd/any-arg world]
            :aimless)

            (ebmd/def-poly take-turn [::directed-bacterium x
            ::ebmd/any-arg world]
            :directed)


            Here, the ::ebmd/any-arg is an arg-spec that matches any argument. The above approach is open to extension just like multi-methods, but does not require you to declare a :type field upfront and is thus more flexible. But, as I said, it is also going to be slower than both multimethods and protocols, so ultimately this is a trade-off.



            Here is the full solution: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj






            share|improve this answer












            You can use example-based multiple dispatch for this, as explained in this blog post. It is certainly not the most performant way to solve this problem, but arguably more flexible than multi-methods as it does not require you to declare a dispatch-method upfront. So it is open for extension to any data representation, even other things than maps. If you need performance, then multi-methods or protocols as you suggest, is probably the way to go.



            First, you need to add a dependency on [bluebell/utils "1.5.0"] and require [bluebell.utils.ebmd :as ebmd]. Then you declare constructors for your data structures (copied from your question) and functions to test those data strucutres:



            (defn new-bacterium [starting-position]
            {:mass 0
            :position starting-position})

            (defn new-directed-bacterium [starting-position starting-directions]
            (-> (new-bacterium starting-position)
            (assoc :direction starting-directions)))

            (defn bacterium? [x]
            (and (map? x)
            (contains? x :position)))

            (defn directed-bacterium? [x]
            (and (bacterium? x)
            (contains? x :direction)))


            Now we are going to register those datastructures as so called arg-specs so that we can use them for dispatch:



            (ebmd/def-arg-spec ::bacterium {:pred bacterium?
            :pos [(new-bacterium [9 8])]
            :neg [3 4]})

            (ebmd/def-arg-spec ::directed-bacterium {:pred directed-bacterium?
            :pos [(new-directed-bacterium [9 8] [3 4])]
            :neg [(new-bacterium [3 4])]})


            For each arg-spec, we need to declare a few example values under the :pos key, and a few non-examples under the :neg key. Those values are used to resolve the fact that a directed-bacterium is more specific than just a bacterium in order for the dispatch to work properly.



            Finally, we are going to define a polymorphic take-turn function. We first declare it, using declare-poly:



            (ebmd/declare-poly take-turn)


            And then, we can provide different implementations for specific arguments:



            (ebmd/def-poly take-turn [::bacterium x
            ::ebmd/any-arg world]
            :aimless)

            (ebmd/def-poly take-turn [::directed-bacterium x
            ::ebmd/any-arg world]
            :directed)


            Here, the ::ebmd/any-arg is an arg-spec that matches any argument. The above approach is open to extension just like multi-methods, but does not require you to declare a :type field upfront and is thus more flexible. But, as I said, it is also going to be slower than both multimethods and protocols, so ultimately this is a trade-off.



            Here is the full solution: https://github.com/jonasseglare/bluebell-utils/blob/archive/2018-11-16-002/test/bluebell/utils/ebmd/bacteria_test.clj







            share|improve this answer












            share|improve this answer



            share|improve this answer










            answered Nov 16 at 9:06









            Rulle

            962414




            962414

























                2














                Dispatching a multimethod by a :type field is indeed polymorphic dispatch that could be done with a protocol, but using multimethods allows you to dispatch on different fields. You can add a second multimethod that dispatches on something other than :type, which might be tricky to accomplish with a protocol (or even multiple protocols).



                Since a multimethod can dispatch on anything, you could use a set as the dispatch value. Here's an alternative approach. It's not fully extensible, since the keys to select are determined within the dispatch function, but it might give you an idea for a better solution:



                (defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

                (defmethod take-turn #{} [this world]
                (println "Aimless turn!"))

                (defmethod take-turn #{:direction} [this world]
                (println "Directed turn!"))





                share|improve this answer


























                  2














                  Dispatching a multimethod by a :type field is indeed polymorphic dispatch that could be done with a protocol, but using multimethods allows you to dispatch on different fields. You can add a second multimethod that dispatches on something other than :type, which might be tricky to accomplish with a protocol (or even multiple protocols).



                  Since a multimethod can dispatch on anything, you could use a set as the dispatch value. Here's an alternative approach. It's not fully extensible, since the keys to select are determined within the dispatch function, but it might give you an idea for a better solution:



                  (defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

                  (defmethod take-turn #{} [this world]
                  (println "Aimless turn!"))

                  (defmethod take-turn #{:direction} [this world]
                  (println "Directed turn!"))





                  share|improve this answer
























                    2












                    2








                    2






                    Dispatching a multimethod by a :type field is indeed polymorphic dispatch that could be done with a protocol, but using multimethods allows you to dispatch on different fields. You can add a second multimethod that dispatches on something other than :type, which might be tricky to accomplish with a protocol (or even multiple protocols).



                    Since a multimethod can dispatch on anything, you could use a set as the dispatch value. Here's an alternative approach. It's not fully extensible, since the keys to select are determined within the dispatch function, but it might give you an idea for a better solution:



                    (defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

                    (defmethod take-turn #{} [this world]
                    (println "Aimless turn!"))

                    (defmethod take-turn #{:direction} [this world]
                    (println "Directed turn!"))





                    share|improve this answer












                    Dispatching a multimethod by a :type field is indeed polymorphic dispatch that could be done with a protocol, but using multimethods allows you to dispatch on different fields. You can add a second multimethod that dispatches on something other than :type, which might be tricky to accomplish with a protocol (or even multiple protocols).



                    Since a multimethod can dispatch on anything, you could use a set as the dispatch value. Here's an alternative approach. It's not fully extensible, since the keys to select are determined within the dispatch function, but it might give you an idea for a better solution:



                    (defmulti take-turn (fn [b _] (clojure.set/intersection #{:direction} (set (keys b)))))

                    (defmethod take-turn #{} [this world]
                    (println "Aimless turn!"))

                    (defmethod take-turn #{:direction} [this world]
                    (println "Directed turn!"))






                    share|improve this answer












                    share|improve this answer



                    share|improve this answer










                    answered Nov 16 at 12:13









                    exupero

                    4,02363752




                    4,02363752























                        1














                        Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.



                        First, we'll start off with an atom to store all of our polymorphic functions:



                        (def polies (atom {}))


                        In usage, the internal structure of the polies would look something like this:



                        {foo ; <- function name
                        {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
                        [pred1 fn1 1 ()]
                        [pred2 fn2 2 '&]]
                        :prefer {:this-pred #{:that-pred :other-pred}}}
                        bar
                        {:dispatch [[pred0 fn0 1 ()]
                        [pred1 fn1 3 ()]]
                        :prefer {:some-pred #{:any-pred}}}}


                        Now, let's make it so that we can prefer predicates (like prefer-method):



                        (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

                        (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
                        (if-let [p (-> @polies (get-in [poly :prefer v1]))]
                        (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

                        (defn- default-sort [v1 v2]
                        (if (= v1 :poly/default)
                        1
                        (if (= v2 :poly/default)
                        -1
                        0)))

                        (defn- pref [poly v1 v2]
                        (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
                        -1
                        (default-sort v1 v2)))

                        (defn- sort-disp [poly]
                        (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

                        (defn prefer [poly v1 v2]
                        (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
                        (sort-disp poly)
                        nil)


                        Now, let's create our dispatch lookup system:



                        (defn- get-disp [poly filter-fn]
                        (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

                        (defn- pred->disp [poly pred]
                        (get-disp poly #(-> % first (= pred))))

                        (defn- pred->poly-fn [poly pred]
                        (-> poly (pred->disp pred) second))

                        (defn- check-args-length [disp args]
                        ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

                        (defn- args-are? [disp args]
                        (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

                        (defn- check-dispatch-on-args [disp args]
                        (if (-> disp first vector?)
                        (-> disp (args-are? args))
                        (-> disp first (apply args))))

                        (defn- disp*args? [disp args]
                        (and (check-args-length disp args)
                        (check-dispatch-on-args disp args)))

                        (defn- args->poly-fn [poly args]
                        (-> poly (get-disp #(disp*args? % args)) second))


                        Next, let's prepare our define macro with some initialization and setup functions:



                        (defn- poly-impl [poly args]
                        (if-let [poly-fn (-> poly (args->poly-fn args))]
                        (-> poly-fn (apply args))
                        (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
                        (-> default-poly-fn (apply args))
                        (throw (ex-info (str "No poly for " poly " with " args) {})))))

                        (defn- remove-disp [poly pred]
                        (when-let [disp (pred->disp poly pred)]
                        (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

                        (defn- til& [args]
                        (count (take-while (partial not= '&) args)))

                        (defn- add-disp [poly poly-fn pred params]
                        (swap! polies update-in [poly :dispatch]
                        #(-> % (or ) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

                        (defn- setup-poly [poly poly-fn pred params]
                        (remove-disp poly pred)
                        (add-disp poly poly-fn pred params)
                        (sort-disp poly))


                        With that, we can finally build our polies by rubbing some macro juice on there:



                        (defmacro defpoly [poly-name pred params body]
                        `(do (when-not (-> ~poly-name quote resolve bound?)
                        (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
                        (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
                        (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
                        ~poly-name))


                        Now you can build arbitrary predicate dispatch:



                        ;; use defpoly like defmethod, but without a defmulti declaration
                        ;; unlike defmethods, all params are passed to defpoly's predicate function
                        (defpoly myinc number? [x] (inc x))

                        (myinc 1)
                        ;#_=> 2

                        (myinc "1")
                        ;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
                        ;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

                        (defpoly myinc :poly/default [x] (inc x))

                        (myinc "1")
                        ;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
                        ;java.lang.String cannot be cast to java.lang.Number

                        (defpoly myinc string? [x] (inc (read-string x)))

                        (myinc "1")
                        ;#_=> 2

                        (defpoly myinc
                        #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
                        [x y & z]
                        (inc (apply + x y z)))

                        (myinc 1 2 3)
                        ;#_=> 7

                        (myinc 1 2 3 "4")
                        ;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
                        ;Wrong number of args (4) passed to: user/eval523/fn--524

                        ; ^ took the :poly/default path


                        And when using your example, we can see:



                        (defn new-bacterium [starting-position]
                        {:mass 0,
                        :position starting-position})

                        (defn new-directed-bacterium [starting-position starting-directions]
                        (-> (new-bacterium starting-position)
                        (assoc :direction starting-directions)))

                        (defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
                        [this world]
                        (println "Directed turn!"))

                        ;; or, if you'd rather use spec
                        (defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
                        [this world]
                        (println "Directed turn!"))

                        (take-turn (new-directed-bacterium [0 0] nil) nil)
                        ;#_=> Directed turn!
                        ;nil

                        (defpoly take-turn :poly/default [this world]
                        (println "Aimless turn!"))

                        (take-turn (new-bacterium [0 0]) nil)
                        ;#_=> Aimless turn!
                        ;nil

                        (defpoly take-turn #(-> %& first :show) [this world]
                        (println :this this :world world))

                        (take-turn (assoc (new-bacterium [0 0]) :show true) nil)
                        ;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
                        ;nil


                        Now, let's try using isa? relationships, a la defmulti:



                        (derive java.util.Map ::collection)

                        (derive java.util.Collection ::collection)

                        ;; always wrap classes in a vector to dispatch off of isa? relationships
                        (defpoly foo [::collection] [c] :a-collection)

                        (defpoly foo [String] [s] :a-string)

                        (foo )
                        ;#_=> :a-collection

                        (foo "bob")
                        ;#_=> :a-string


                        And of course we can use prefer to disambiguate relationships:



                        (derive ::rect ::shape)

                        (defpoly bar [::rect ::shape] [x y] :rect-shape)

                        (defpoly bar [::shape ::rect] [x y] :shape-rect)

                        (bar ::rect ::rect)
                        ;#_=> :rect-shape

                        (prefer bar [::shape ::rect] [::rect ::shape])

                        (bar ::rect ::rect)
                        ;#_=> :shape-rect


                        Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.






                        share|improve this answer




























                          1














                          Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.



                          First, we'll start off with an atom to store all of our polymorphic functions:



                          (def polies (atom {}))


                          In usage, the internal structure of the polies would look something like this:



                          {foo ; <- function name
                          {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
                          [pred1 fn1 1 ()]
                          [pred2 fn2 2 '&]]
                          :prefer {:this-pred #{:that-pred :other-pred}}}
                          bar
                          {:dispatch [[pred0 fn0 1 ()]
                          [pred1 fn1 3 ()]]
                          :prefer {:some-pred #{:any-pred}}}}


                          Now, let's make it so that we can prefer predicates (like prefer-method):



                          (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

                          (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
                          (if-let [p (-> @polies (get-in [poly :prefer v1]))]
                          (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

                          (defn- default-sort [v1 v2]
                          (if (= v1 :poly/default)
                          1
                          (if (= v2 :poly/default)
                          -1
                          0)))

                          (defn- pref [poly v1 v2]
                          (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
                          -1
                          (default-sort v1 v2)))

                          (defn- sort-disp [poly]
                          (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

                          (defn prefer [poly v1 v2]
                          (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
                          (sort-disp poly)
                          nil)


                          Now, let's create our dispatch lookup system:



                          (defn- get-disp [poly filter-fn]
                          (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

                          (defn- pred->disp [poly pred]
                          (get-disp poly #(-> % first (= pred))))

                          (defn- pred->poly-fn [poly pred]
                          (-> poly (pred->disp pred) second))

                          (defn- check-args-length [disp args]
                          ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

                          (defn- args-are? [disp args]
                          (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

                          (defn- check-dispatch-on-args [disp args]
                          (if (-> disp first vector?)
                          (-> disp (args-are? args))
                          (-> disp first (apply args))))

                          (defn- disp*args? [disp args]
                          (and (check-args-length disp args)
                          (check-dispatch-on-args disp args)))

                          (defn- args->poly-fn [poly args]
                          (-> poly (get-disp #(disp*args? % args)) second))


                          Next, let's prepare our define macro with some initialization and setup functions:



                          (defn- poly-impl [poly args]
                          (if-let [poly-fn (-> poly (args->poly-fn args))]
                          (-> poly-fn (apply args))
                          (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
                          (-> default-poly-fn (apply args))
                          (throw (ex-info (str "No poly for " poly " with " args) {})))))

                          (defn- remove-disp [poly pred]
                          (when-let [disp (pred->disp poly pred)]
                          (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

                          (defn- til& [args]
                          (count (take-while (partial not= '&) args)))

                          (defn- add-disp [poly poly-fn pred params]
                          (swap! polies update-in [poly :dispatch]
                          #(-> % (or ) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

                          (defn- setup-poly [poly poly-fn pred params]
                          (remove-disp poly pred)
                          (add-disp poly poly-fn pred params)
                          (sort-disp poly))


                          With that, we can finally build our polies by rubbing some macro juice on there:



                          (defmacro defpoly [poly-name pred params body]
                          `(do (when-not (-> ~poly-name quote resolve bound?)
                          (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
                          (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
                          (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
                          ~poly-name))


                          Now you can build arbitrary predicate dispatch:



                          ;; use defpoly like defmethod, but without a defmulti declaration
                          ;; unlike defmethods, all params are passed to defpoly's predicate function
                          (defpoly myinc number? [x] (inc x))

                          (myinc 1)
                          ;#_=> 2

                          (myinc "1")
                          ;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
                          ;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

                          (defpoly myinc :poly/default [x] (inc x))

                          (myinc "1")
                          ;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
                          ;java.lang.String cannot be cast to java.lang.Number

                          (defpoly myinc string? [x] (inc (read-string x)))

                          (myinc "1")
                          ;#_=> 2

                          (defpoly myinc
                          #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
                          [x y & z]
                          (inc (apply + x y z)))

                          (myinc 1 2 3)
                          ;#_=> 7

                          (myinc 1 2 3 "4")
                          ;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
                          ;Wrong number of args (4) passed to: user/eval523/fn--524

                          ; ^ took the :poly/default path


                          And when using your example, we can see:



                          (defn new-bacterium [starting-position]
                          {:mass 0,
                          :position starting-position})

                          (defn new-directed-bacterium [starting-position starting-directions]
                          (-> (new-bacterium starting-position)
                          (assoc :direction starting-directions)))

                          (defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
                          [this world]
                          (println "Directed turn!"))

                          ;; or, if you'd rather use spec
                          (defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
                          [this world]
                          (println "Directed turn!"))

                          (take-turn (new-directed-bacterium [0 0] nil) nil)
                          ;#_=> Directed turn!
                          ;nil

                          (defpoly take-turn :poly/default [this world]
                          (println "Aimless turn!"))

                          (take-turn (new-bacterium [0 0]) nil)
                          ;#_=> Aimless turn!
                          ;nil

                          (defpoly take-turn #(-> %& first :show) [this world]
                          (println :this this :world world))

                          (take-turn (assoc (new-bacterium [0 0]) :show true) nil)
                          ;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
                          ;nil


                          Now, let's try using isa? relationships, a la defmulti:



                          (derive java.util.Map ::collection)

                          (derive java.util.Collection ::collection)

                          ;; always wrap classes in a vector to dispatch off of isa? relationships
                          (defpoly foo [::collection] [c] :a-collection)

                          (defpoly foo [String] [s] :a-string)

                          (foo )
                          ;#_=> :a-collection

                          (foo "bob")
                          ;#_=> :a-string


                          And of course we can use prefer to disambiguate relationships:



                          (derive ::rect ::shape)

                          (defpoly bar [::rect ::shape] [x y] :rect-shape)

                          (defpoly bar [::shape ::rect] [x y] :shape-rect)

                          (bar ::rect ::rect)
                          ;#_=> :rect-shape

                          (prefer bar [::shape ::rect] [::rect ::shape])

                          (bar ::rect ::rect)
                          ;#_=> :shape-rect


                          Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.






                          share|improve this answer


























                            1












                            1








                            1






                            Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.



                            First, we'll start off with an atom to store all of our polymorphic functions:



                            (def polies (atom {}))


                            In usage, the internal structure of the polies would look something like this:



                            {foo ; <- function name
                            {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
                            [pred1 fn1 1 ()]
                            [pred2 fn2 2 '&]]
                            :prefer {:this-pred #{:that-pred :other-pred}}}
                            bar
                            {:dispatch [[pred0 fn0 1 ()]
                            [pred1 fn1 3 ()]]
                            :prefer {:some-pred #{:any-pred}}}}


                            Now, let's make it so that we can prefer predicates (like prefer-method):



                            (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

                            (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
                            (if-let [p (-> @polies (get-in [poly :prefer v1]))]
                            (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

                            (defn- default-sort [v1 v2]
                            (if (= v1 :poly/default)
                            1
                            (if (= v2 :poly/default)
                            -1
                            0)))

                            (defn- pref [poly v1 v2]
                            (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
                            -1
                            (default-sort v1 v2)))

                            (defn- sort-disp [poly]
                            (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

                            (defn prefer [poly v1 v2]
                            (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
                            (sort-disp poly)
                            nil)


                            Now, let's create our dispatch lookup system:



                            (defn- get-disp [poly filter-fn]
                            (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

                            (defn- pred->disp [poly pred]
                            (get-disp poly #(-> % first (= pred))))

                            (defn- pred->poly-fn [poly pred]
                            (-> poly (pred->disp pred) second))

                            (defn- check-args-length [disp args]
                            ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

                            (defn- args-are? [disp args]
                            (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

                            (defn- check-dispatch-on-args [disp args]
                            (if (-> disp first vector?)
                            (-> disp (args-are? args))
                            (-> disp first (apply args))))

                            (defn- disp*args? [disp args]
                            (and (check-args-length disp args)
                            (check-dispatch-on-args disp args)))

                            (defn- args->poly-fn [poly args]
                            (-> poly (get-disp #(disp*args? % args)) second))


                            Next, let's prepare our define macro with some initialization and setup functions:



                            (defn- poly-impl [poly args]
                            (if-let [poly-fn (-> poly (args->poly-fn args))]
                            (-> poly-fn (apply args))
                            (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
                            (-> default-poly-fn (apply args))
                            (throw (ex-info (str "No poly for " poly " with " args) {})))))

                            (defn- remove-disp [poly pred]
                            (when-let [disp (pred->disp poly pred)]
                            (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

                            (defn- til& [args]
                            (count (take-while (partial not= '&) args)))

                            (defn- add-disp [poly poly-fn pred params]
                            (swap! polies update-in [poly :dispatch]
                            #(-> % (or ) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

                            (defn- setup-poly [poly poly-fn pred params]
                            (remove-disp poly pred)
                            (add-disp poly poly-fn pred params)
                            (sort-disp poly))


                            With that, we can finally build our polies by rubbing some macro juice on there:



                            (defmacro defpoly [poly-name pred params body]
                            `(do (when-not (-> ~poly-name quote resolve bound?)
                            (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
                            (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
                            (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
                            ~poly-name))


                            Now you can build arbitrary predicate dispatch:



                            ;; use defpoly like defmethod, but without a defmulti declaration
                            ;; unlike defmethods, all params are passed to defpoly's predicate function
                            (defpoly myinc number? [x] (inc x))

                            (myinc 1)
                            ;#_=> 2

                            (myinc "1")
                            ;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
                            ;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

                            (defpoly myinc :poly/default [x] (inc x))

                            (myinc "1")
                            ;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
                            ;java.lang.String cannot be cast to java.lang.Number

                            (defpoly myinc string? [x] (inc (read-string x)))

                            (myinc "1")
                            ;#_=> 2

                            (defpoly myinc
                            #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
                            [x y & z]
                            (inc (apply + x y z)))

                            (myinc 1 2 3)
                            ;#_=> 7

                            (myinc 1 2 3 "4")
                            ;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
                            ;Wrong number of args (4) passed to: user/eval523/fn--524

                            ; ^ took the :poly/default path


                            And when using your example, we can see:



                            (defn new-bacterium [starting-position]
                            {:mass 0,
                            :position starting-position})

                            (defn new-directed-bacterium [starting-position starting-directions]
                            (-> (new-bacterium starting-position)
                            (assoc :direction starting-directions)))

                            (defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
                            [this world]
                            (println "Directed turn!"))

                            ;; or, if you'd rather use spec
                            (defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
                            [this world]
                            (println "Directed turn!"))

                            (take-turn (new-directed-bacterium [0 0] nil) nil)
                            ;#_=> Directed turn!
                            ;nil

                            (defpoly take-turn :poly/default [this world]
                            (println "Aimless turn!"))

                            (take-turn (new-bacterium [0 0]) nil)
                            ;#_=> Aimless turn!
                            ;nil

                            (defpoly take-turn #(-> %& first :show) [this world]
                            (println :this this :world world))

                            (take-turn (assoc (new-bacterium [0 0]) :show true) nil)
                            ;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
                            ;nil


                            Now, let's try using isa? relationships, a la defmulti:



                            (derive java.util.Map ::collection)

                            (derive java.util.Collection ::collection)

                            ;; always wrap classes in a vector to dispatch off of isa? relationships
                            (defpoly foo [::collection] [c] :a-collection)

                            (defpoly foo [String] [s] :a-string)

                            (foo )
                            ;#_=> :a-collection

                            (foo "bob")
                            ;#_=> :a-string


                            And of course we can use prefer to disambiguate relationships:



                            (derive ::rect ::shape)

                            (defpoly bar [::rect ::shape] [x y] :rect-shape)

                            (defpoly bar [::shape ::rect] [x y] :shape-rect)

                            (bar ::rect ::rect)
                            ;#_=> :rect-shape

                            (prefer bar [::shape ::rect] [::rect ::shape])

                            (bar ::rect ::rect)
                            ;#_=> :shape-rect


                            Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.






                            share|improve this answer














                            Fast paths exist for a reason, but Clojure doesn't stop you from doing anything you want to do, per say, including ad hoc predicate dispatch. The world is definitely your oyster. Observe this super quick and dirty example below.



                            First, we'll start off with an atom to store all of our polymorphic functions:



                            (def polies (atom {}))


                            In usage, the internal structure of the polies would look something like this:



                            {foo ; <- function name
                            {:dispatch [[pred0 fn0 1 ()] ; <- if (pred0 args) do (fn0 args)
                            [pred1 fn1 1 ()]
                            [pred2 fn2 2 '&]]
                            :prefer {:this-pred #{:that-pred :other-pred}}}
                            bar
                            {:dispatch [[pred0 fn0 1 ()]
                            [pred1 fn1 3 ()]]
                            :prefer {:some-pred #{:any-pred}}}}


                            Now, let's make it so that we can prefer predicates (like prefer-method):



                            (defn- get-parent [pfn x] (->> (parents x) (filter pfn) first))

                            (defn- in-this-or-parent-prefs? [poly v1 v2 f1 f2]
                            (if-let [p (-> @polies (get-in [poly :prefer v1]))]
                            (or (contains? p v2) (get-parent f1 v2) (get-parent f2 v1))))

                            (defn- default-sort [v1 v2]
                            (if (= v1 :poly/default)
                            1
                            (if (= v2 :poly/default)
                            -1
                            0)))

                            (defn- pref [poly v1 v2]
                            (if (-> poly (in-this-or-parent-prefs? v1 v2 #(pref poly v1 %) #(pref poly % v2)))
                            -1
                            (default-sort v1 v2)))

                            (defn- sort-disp [poly]
                            (swap! polies update-in [poly :dispatch] #(->> % (sort-by first (partial pref poly)) vec)))

                            (defn prefer [poly v1 v2]
                            (swap! polies update-in [poly :prefer v1] #(-> % (or #{}) (conj v2)))
                            (sort-disp poly)
                            nil)


                            Now, let's create our dispatch lookup system:



                            (defn- get-disp [poly filter-fn]
                            (-> @polies (get-in [poly :dispatch]) (->> (filter filter-fn)) first))

                            (defn- pred->disp [poly pred]
                            (get-disp poly #(-> % first (= pred))))

                            (defn- pred->poly-fn [poly pred]
                            (-> poly (pred->disp pred) second))

                            (defn- check-args-length [disp args]
                            ((if (= '& (-> disp (nth 3) first)) >= =) (count args) (nth disp 2)))

                            (defn- args-are? [disp args]
                            (or (isa? (vec args) (first disp)) (isa? (mapv class args) (first disp))))

                            (defn- check-dispatch-on-args [disp args]
                            (if (-> disp first vector?)
                            (-> disp (args-are? args))
                            (-> disp first (apply args))))

                            (defn- disp*args? [disp args]
                            (and (check-args-length disp args)
                            (check-dispatch-on-args disp args)))

                            (defn- args->poly-fn [poly args]
                            (-> poly (get-disp #(disp*args? % args)) second))


                            Next, let's prepare our define macro with some initialization and setup functions:



                            (defn- poly-impl [poly args]
                            (if-let [poly-fn (-> poly (args->poly-fn args))]
                            (-> poly-fn (apply args))
                            (if-let [default-poly-fn (-> poly (pred->poly-fn :poly/default))]
                            (-> default-poly-fn (apply args))
                            (throw (ex-info (str "No poly for " poly " with " args) {})))))

                            (defn- remove-disp [poly pred]
                            (when-let [disp (pred->disp poly pred)]
                            (swap! polies update-in [poly :dispatch] #(->> % (remove #{disp}) vec))))

                            (defn- til& [args]
                            (count (take-while (partial not= '&) args)))

                            (defn- add-disp [poly poly-fn pred params]
                            (swap! polies update-in [poly :dispatch]
                            #(-> % (or ) (conj [pred poly-fn (til& params) (filter #{'&} params)]))))

                            (defn- setup-poly [poly poly-fn pred params]
                            (remove-disp poly pred)
                            (add-disp poly poly-fn pred params)
                            (sort-disp poly))


                            With that, we can finally build our polies by rubbing some macro juice on there:



                            (defmacro defpoly [poly-name pred params body]
                            `(do (when-not (-> ~poly-name quote resolve bound?)
                            (defn ~poly-name [& args#] (poly-impl ~poly-name args#)))
                            (let [poly-fn# (fn ~(symbol (str poly-name "-poly")) ~params ~body)]
                            (setup-poly ~poly-name poly-fn# ~pred (quote ~params)))
                            ~poly-name))


                            Now you can build arbitrary predicate dispatch:



                            ;; use defpoly like defmethod, but without a defmulti declaration
                            ;; unlike defmethods, all params are passed to defpoly's predicate function
                            (defpoly myinc number? [x] (inc x))

                            (myinc 1)
                            ;#_=> 2

                            (myinc "1")
                            ;#_=> Execution error (ExceptionInfo) at user$poly_impl/invokeStatic (REPL:6).
                            ;No poly for user$eval187$myinc__188@5c8eee0f with ("1")

                            (defpoly myinc :poly/default [x] (inc x))

                            (myinc "1")
                            ;#_=> Execution error (ClassCastException) at user$eval245$fn__246/invoke (REPL:1).
                            ;java.lang.String cannot be cast to java.lang.Number

                            (defpoly myinc string? [x] (inc (read-string x)))

                            (myinc "1")
                            ;#_=> 2

                            (defpoly myinc
                            #(and (number? %1) (number? %2) (->> %& (filter (complement number?)) empty?))
                            [x y & z]
                            (inc (apply + x y z)))

                            (myinc 1 2 3)
                            ;#_=> 7

                            (myinc 1 2 3 "4")
                            ;#_=> Execution error (ArityException) at user$poly_impl/invokeStatic (REPL:5).
                            ;Wrong number of args (4) passed to: user/eval523/fn--524

                            ; ^ took the :poly/default path


                            And when using your example, we can see:



                            (defn new-bacterium [starting-position]
                            {:mass 0,
                            :position starting-position})

                            (defn new-directed-bacterium [starting-position starting-directions]
                            (-> (new-bacterium starting-position)
                            (assoc :direction starting-directions)))

                            (defpoly take-turn (fn [b _] (-> b keys set (contains? :direction)))
                            [this world]
                            (println "Directed turn!"))

                            ;; or, if you'd rather use spec
                            (defpoly take-turn (fn [b _] (->> b (s/valid? (s/keys :req-un [::direction])))
                            [this world]
                            (println "Directed turn!"))

                            (take-turn (new-directed-bacterium [0 0] nil) nil)
                            ;#_=> Directed turn!
                            ;nil

                            (defpoly take-turn :poly/default [this world]
                            (println "Aimless turn!"))

                            (take-turn (new-bacterium [0 0]) nil)
                            ;#_=> Aimless turn!
                            ;nil

                            (defpoly take-turn #(-> %& first :show) [this world]
                            (println :this this :world world))

                            (take-turn (assoc (new-bacterium [0 0]) :show true) nil)
                            ;#_=> :this {:mass 0, :position [0 0], :show true} :world nil
                            ;nil


                            Now, let's try using isa? relationships, a la defmulti:



                            (derive java.util.Map ::collection)

                            (derive java.util.Collection ::collection)

                            ;; always wrap classes in a vector to dispatch off of isa? relationships
                            (defpoly foo [::collection] [c] :a-collection)

                            (defpoly foo [String] [s] :a-string)

                            (foo )
                            ;#_=> :a-collection

                            (foo "bob")
                            ;#_=> :a-string


                            And of course we can use prefer to disambiguate relationships:



                            (derive ::rect ::shape)

                            (defpoly bar [::rect ::shape] [x y] :rect-shape)

                            (defpoly bar [::shape ::rect] [x y] :shape-rect)

                            (bar ::rect ::rect)
                            ;#_=> :rect-shape

                            (prefer bar [::shape ::rect] [::rect ::shape])

                            (bar ::rect ::rect)
                            ;#_=> :shape-rect


                            Again, the world's your oyster! There's nothing stopping you from extending the language in any direction you want.







                            share|improve this answer














                            share|improve this answer



                            share|improve this answer








                            edited Nov 20 at 19:26

























                            answered Nov 17 at 19:52









                            John Newman

                            113




                            113






























                                draft saved

                                draft discarded




















































                                Thanks for contributing an answer to Stack Overflow!


                                • Please be sure to answer the question. Provide details and share your research!

                                But avoid



                                • Asking for help, clarification, or responding to other answers.

                                • Making statements based on opinion; back them up with references or personal experience.


                                To learn more, see our tips on writing great answers.





                                Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


                                Please pay close attention to the following guidance:


                                • Please be sure to answer the question. Provide details and share your research!

                                But avoid



                                • Asking for help, clarification, or responding to other answers.

                                • Making statements based on opinion; back them up with references or personal experience.


                                To learn more, see our tips on writing great answers.




                                draft saved


                                draft discarded














                                StackExchange.ready(
                                function () {
                                StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53329709%2fdispatching-function-calls-on-different-formats-of-maps%23new-answer', 'question_page');
                                }
                                );

                                Post as a guest















                                Required, but never shown





















































                                Required, but never shown














                                Required, but never shown












                                Required, but never shown







                                Required, but never shown

































                                Required, but never shown














                                Required, but never shown












                                Required, but never shown







                                Required, but never shown







                                Popular posts from this blog

                                Biblatex bibliography style without URLs when DOI exists (in Overleaf with Zotero bibliography)

                                ComboBox Display Member on multiple fields

                                Is it possible to collect Nectar points via Trainline?