Clojure: ensuring multimethods are required


Multimethods are fantastic. They give you polymorphism without objects or classes (the best part of Object Oriented without the baggage), multiple dispatch, dynamic dispatch and strong decoupling that allows you to extend code without modifying it (open closed principle), this even extends to third party code. This decoupling is so good that it's not unheard of to deploy your system without all the defmethod extensions being required! This post will teach you how to prevent this.

The printer namespace defines the print multimethod which dispatches on the :type of it's input.

(ns ensuring-multimethods-are-required.printer
  (:refer-clojure :exclude [print]))

(defmulti print :type)

(defmethod print :default [{:keys [text]}] (println text))

A namespace, called shout, defines a p/print method that upper cases :text before printing.

(ns ensuring-multimethods-are-required.shout
  (:require [ensuring-multimethods-are-required.printer :as p]
            [clojure.string :as str]))

(defmethod p/print :shout [{:keys [text]}]
  (println (str/upper-case text)))

Another namespace, called whisper, defines a p/print method that lower cases :text before printing.

(ns ensuring-multimethods-are-required.whisper
  (:require [ensuring-multimethods-are-required.printer :as p]
            [clojure.string :as str]))

(defmethod p/print :whisper [{:keys [text]}]
  (println (str/lower-case text)))

In the core namespace the p/print multimethod is called with a series of data.

At the top of the file (after the namespace declaration) an assertion checks that the p/print multimethod has the expected multimethods implementation registered to it.

(ns ensuring-multimethods-are-required.core
  (:require
   [ensuring-multimethods-are-required.printer :as p]))

(let [loaded-methods (-> p/print methods keys set)
      expected-methods #{:default :shout :whisper}]
  (assert (= expected-methods loaded-methods)
          (str expected-methods " =/= " loaded-methods)))

(run! p/print [{:text "Hello"}
               {:type :shout :text "Hello"}
               {:type :whisper :text "Hello"}])

In this case two of the desired implementations are missing and an error is thrown when trying to compile the namespace.

Syntax error (AssertionError) compiling at (ensuring_multimethods_are_required/core.clj:5:1).
Assert failed: #{:shout :default :whisper} =/= #{:default}
(= expected-methods loaded-methods)

The missing shout and whisper namespaces are added to the namespace deceleration.

(ns ensuring-multimethods-are-required.core
  (:require
   [ensuring-multimethods-are-required.whisper]
   [ensuring-multimethods-are-required.shout]
   [ensuring-multimethods-are-required.printer :as p]))

(let [loaded-methods (-> p/print methods keys set)
      expected-methods #{:default :shout :whisper}]
  (assert (= expected-methods loaded-methods)
          (str expected-methods " =/= " loaded-methods)))

(run! p/print [{:text "Hello"}
               {:type :shout :text "Hello"}
               {:type :whisper :text "Hello"}])

Everything now works as expected.

Hello
HELLO
hello

nil

This trick helps avoid unexpected behaviour caused by missing multimethod implementations.

The full example project can be found here.