DEV Community

Cover image for Understanding Clojure Multimethods
Kelvin Mai
Kelvin Mai

Posted on

Understanding Clojure Multimethods

It took me quite a while to wrap my head around clojure's multimethods, which is clojure's version of pattern matching and polymorphism. Pattern matching is a pretty core part of how functional programming languages to combat the ambiguaty of null. The big draw to it in typed languages like scala and ocaml is to exhaust all possibilites, however clojure is a dynamically typed language so there is a different interest here. We're interested in the branching paths, kinda like an expanded if statement, to call a different function depending on the condition. Javascript and other C-like languages can do a pretty good impression of this with the switch statement.

const switchFn = (condition) => {
  switch (condition) {
    case "true":
      console.log(true);
      break;
    case "false":
      console.log(false);
      break;
    default:
      console.log("maybe?");
      break;
  }
};

Here is the clojure equivalent of a switch case, using clojure keywords (used by the ':' syntax) instead of strings as the conditions. And it may not be the best example for boolean cases, as you would opt for a switch when you have more than 2 possibilities. But this is the

(defn switch-fn [condition]
  (case condition
    :true (prn true)
    :false (prn false)
    :default (prn "maybe?")))

But the downside of the case function is that to update the functionality you would have to edit the function altogether. It may not seem like that big of a deal until polymorphism is taken into account. Say for example you want to add an additional case to a third party library, which may be almost impossible. This is an extreme case, but it does illustrate the limitations of a switch case.

Multimethods

And that's where clojure's multimethods come in. Using the defmulti and defmethod macros we can define both the switch and cases separately.

(defmulti factorial identity)

(defmethod factorial 0 [_]  1)
(defmethod factorial :default [num]
    (* num (factorial (dec num))))

(factorial 0) ; => 1
(factorial 1) ; => 1
(factorial 3) ; => 6
(factorial 7) ; => 5040

This is an example of implementing a factorial function with multimethods instead of the more typical recursive alternative. The defmulti macro's form structure first takes in the name of the multimethod, and subsequently each defmethod's first param must be the name of the same as the one in defmulti so that clojure knows which multimethod it belongs to. The second argument of defmulti defines the function on how to determine wich method to use, here it's provided the identity method so whatever number is provided will be the case. In the methods, the second is the cases and uses a :default as the default case and the last param is the return value. The list param is what confused me, it will match the same input as the defmulti so it will always include the cases, in this factorial example it's not too complex as the number is also the condition. But if you want a React/Redux style action dispatch system it will end up looking like this instead.

(defmulti app-reducer
  (fn [state action] (first action)))

(defmethod app-reducer
  :set-list [state [action-type payload]]
  (or payload state))

(defmethod app-reducer
  :add-to-list [state [action-type payload]]
  (conj state payload))

;; calling the actions here
(app-reducer state [:set-list [1 2 3]])
(app-reducer state [:add-to-list 4])

With the redux style reducer you will always have 2 arguments, the state and action, but the action is then divided into it's action type and payload. So to maintain arity (number of arguments) the action here is embedded in a list of it's own, being destructured in each of the defmethods. In the defmulti the function here returns just the action type to determine which condition to use, which is why it returns the first in the action list. But in the methods return value we are only interested in the payload, so we ignore the action type as it has already been used to determine which method.

Follow and support me

Top comments (0)