Choosing the right kind of function dispatch

Greetings all! I’m developing an application which takes files as input and processes these differently based on the file’s metadata. I imagine this is pretty common among users of functional programming, and it’s not the first time I have written a program with this kind of architecture. However, I’m wondering which of the myriad of ways to do this is most optimal, and this is where you come in!

Out of the options below, what do you think are the pros and cons of them? Do you have any ways that you’ve developed that you think would work well? I’m keen to hear your thoughts :grinning:

For sake of example, consider an application which reads data about what sort of email notification to send a user.

Option 1 - ‘function pointers’

There’s a number of functions, emails/send-promotion! etc., that should be called to respond to different events. You have a map from event type to function…

(def dispatch-map
     {:sale-confirmation emails/send-sales-confirmation!
      :promotion emails/send-promotion!
      :welcome emails/send-welcome!})

And when the event is received, the right function is called:

(defn dispatch-function [event]
  (when-some [function (resolve (dispatch-map event))]
    (function)))

Of course, the dispatch-map could also be an argument to dispatch-function rather than a global binding.

Option 2 - pattern matching

There’s the same set of functions, but no dispatch-map. Instead, the functions are called directly from the dispatch-function:

(defn dispatch-function-alt [event]
  (condp = event
    :sale-confirmation (emails/send-sales-confirmation!)
    :promotion (emails/send-promotion!)
    :welcome (emails/send-welcome!)
    nil))

Option 3 - multimethods

There’s just a single call to defmulti to set up the dispatching:

(defmulti dispatch identity)

Then, there are calls to defmethod to set up the function for each value:

(defmethod dispatch :sales [_] (
;; the body of emails/send-sales-confirmation!
))
(defmethod dispatch :promotion [_] (
;; the body of emails/send-promotion!
))
(defmethod dispatch :welcome [_] (
;; the body of emails/send-welcome!
))
(defmethod dispatch :default [_] nil)

Looking forward to hearing what you think! :smiley:

1 Like

For option 2, you can use case instead of condp and it will also be faster.

Hum, I don’t think anyone of them is better than the others, they provide different properties.

Option 1 is good if you want the dispatch to be configuration, move that in some EDN file, let a user pick and choose by providing their own config.

I’d say if you don’t want that, I find it is harder to track in the code. Figuring out what calls what is a bit more difficult to follow, find where and what dispatch map will be passed in, what are the functions it points too, where are those defined, what is the contract for those functions to follow (input/output), etc.

Option 2, when using a case will be fastest I believe. If you care about dispatch performance. It’s very easy to follow in code what it’s doing. You get this one place that clearly shows you what is dispatched to wait function, you can easily goto-definition on them in your IDE. You also know that’s the full set of them, no hidden ones that might have been defined somewhere else you’re not finding currently. So if you want to know the set of supported event types it’s clearly all there.

That said, it can’t be externally extended, and it can’t be made into a configuration.

Option 3 let’s you extend the set of supported events externally, similar to option 1, but it is a bit more organized. You can still go-to-definition and find the defmulti, and then more easily search for all it’s defmultis. It’s not as easy to track down the total set as option 2, but if you need to provide extension by others, say you’re a library or a separate package, it’s a good choice.

You still can’t make it configurable as config though, which is the downside compared to option 1.

Personally, I would start with Option 2, it is the simplest in my opinion, is the most tractable, and has the least surface area. But it is the most restrictive, so as you realize you need more features, like make it externally extendable, just refactor into a defmultis is easy. And if you realize you need it to be externally configurable, refactor it into a dispatch-map is also easy.

6 Likes

In addition to other comments, keep in mind that these options aren‘t necessarily mutually exclusive. As a simple example for that, you can combine approach 1 and 3 into a pretty flexible system:

Have a dispatch-map and -fn like in your post‘s first example, but make the function call a multimethod when resolution from the map fails. This way you have a sort-of-overwrite mechanism apart from what multimethods offer (which could be for extension from the outside, with the „overwrite“ map ensuring some reserved names and/or functionality around the multimethod).

If you spin this further, combining multiple multimethods in your dispatch-fn approaches protocols conceptually, and it’s good to think about whether or not they are a good tool for a given purpose. There‘s a lot of pros and cons to „building your own“ dispatch ensemble, as there are to using Clojure‘s facilities „as-is“ (records, multimethods, protocols, …) or on their own.

@didibus, thanks for pointing out the performance with case instead of condp - I tried it on one of my projects which uses this kind of dispatch logic, and there was indeed a measurable speed-up :smiley:

I think your appraisal makes a lot of sense, especially how you can move from Option 2 to Option 3 easily.

@madbonkey - thanks; that’s an interesting idea. I hadn’t considered combining them, but I can see how that might be useful for letting the user override the control flow at runtime, whilst primarily using the multimethods. From what I’ve read, methods have come in and gone out of fashion a few times in the Clojure community, so I can see what you mean about multiple pros and cons to weigh up!

I made polymethods for fun a while back GitHub - johnmn3/dispacio: A predicate stack dispatch system for Clojure/Script

It’s similar to multimethods but let’s each method add its own dispatch function, rather than adding static values to be matched against the return value of a single dispatch function.

So you can use string? or int? or whatever predicate you want. Like:

(defp f-inc int? [x] (float (inc  x)))

(defp f-inc string? [x] (float (inc (read-string x))))

Or even dispatch off of specs or whatever else you want:

(require '[clojure.spec.alpha :as s])
(s/def :animal/kind string?)
(s/def :animal/says string?)
(s/def :animal/common (s/keys :req [:animal/kind :animal/says]))
(s/def :dog/tail? boolean?)
(s/def :dog/breed string?)
(s/def :cat/breed string?)
(s/def :animal/dog (s/merge :animal/common
                            (s/keys :req [:dog/tail? :dog/breed])))
(s/def :animal/cat (s/merge :animal/common
                            (s/keys :req [:cat/breed])))

With specs defined, make your polymethods:

(defp make-noise #(s/valid? :animal/dog %)
  [animal]
  (println "A" (:dog/breed animal) "barks" (:animal/says animal)))

(defp make-noise #(s/valid? :animal/cat %)
  [animal]
  (println "A" (:cat/breed animal) "meows" (:animal/says animal)))

And then you can dispatch on valid data:

(make-noise
  {:animal/kind "dog"
   :animal/says "woof"
   :dog/tail? true
   :dog/breed "retriever"})
;#_=> A retriever barks woof
(make-noise
  {:animal/kind "cat"
   :animal/says "brrr"
   :cat/breed "siamese"})
;#_=> A siamese meows brrr

Fun for when you want to experiment with different dispatch strategies based on the truthiness of arbitrary functions.

1 Like

Realized there was a bug in the library, so deleted the suggestion. Will update back here when it’s fixed. (edit: okay, updating the repo with the fix)

So to make it more applicable to your example, you could for instance start off with an emails namespace:

(ns emails
  (:require [dispacio.alpha.core :refer [defp]]))

(defn send-email! [email]
  (println :sending-email :msg (:msg email)))

(defp send! :poly/default
  [email]
  (println :don't-know-what-to-do-with-this email))

Then implement the polymethods across different namespaces. We’re just looking at top level keywords here but as shown above we could be using full specs here to dispatch on s/valid?.

(ns promotion
  (:require [dispacio.alpha.core :refer [defp]]
            [emails :as emails]))

(defp emails/send! #(-> % :email-type (= :promotion))
  [email]
  (emails/send-email!
   (assoc email
          :msg (str "Congrats! You got a promotion " (:name email) "!"))))
(ns welcome
  (:require [dispacio.alpha.core :refer [defp]]
            [emails :as emails]))

(defp emails/send! #(-> % :file-type namespace (= "welcome"))
  [email]
  (emails/send-email!
   (assoc email
          :msg (str "Welcome! Glad you're here " (:name email) "!"))))
(ns confirmation
  (:require [dispacio.alpha.core :refer [defp]]
            [emails :as emails]))

(defp emails/send! #(-> % :file-kind (= :confirmation))
  [email]
  (emails/send-email!
   (assoc email
          :msg (str "Confirmed! It's true " (:name email) "."))))

And then you could just call the email namespace from jobs namespace or whatever:

(ns jobs
  (:require [emails :as emails]))

(def files ; <- dispatching on heterogeneous/inconsistent data
  [{:file-kind  :confirmation :name "John"}
   {:file-type  :welcome/new  :name "Mary"}
   {:email-type :promotion    :name "Jules"}])

(->> files (map emails/send!))

;; clj꞉jobs꞉> 
; :sending-email :msg Confirmed! It's true John.
; :sending-email :msg Welcome! Glad you're here Mary!
; :sending-email :msg Congrats! You got a promotion Jules!
;(nil nil nil)

This gives you the cross-namespace abilities of defmethods with the flexibility of arbitrary predicate dispatch.

Hi, this looks interesting. One or two questions came to mind.

  • Is the order in which fn’s get an opportunity to receive a value deterministic? Eg if there is a value for which two fn’s might match truthy, which one gets the first stab at it?
  • If multiple fn’s match, do they all receive the value? (vs only the “first” fn that matches)
1 Like

Hi @pieterbreed thanks. Yeah, it is deterministic - first truthy predicate wins, in the order they’re defined (though I’m thinking about reversing the order, so you can overwrite them while repling). Only first match executes.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.