How to enable users to override selected functions in a ns?

I would like to enable users of the Cryogen static blog generator to override selected functions (in its compiler ns) so that they can for example provide extra data to the page being rendered. How to make this possible?

1. binding

The simplest solution is to use binding to ad-hoc override the functions of interest:

(binding [compile-tags-page my-tags-page] (compile-blog))

however, as former OOP dev, it seems “dirty” to override random functions somewhere deep in a function call :slight_smile: though it also looks as the simplest solution.

2. protocols

The standard Clojure solution for supporting alternative implementations is protocols - I could modify the root function to take an implementation of a new Compiler protocol. That is nice and clean - but the problem is that there are potentially many functions of interest but I only want to override a single one.
I could solve that by making my implementation delegate all other functions to the original impl but it still forces me to declare that for each:

(defprotocol Compiler
  (compile-tags-page [_])
  (compile-tags [_])
  (compile-posts [_])
  #_etc...)

(defrecord MyCompiler []
  Compiler
 (compile-tags-page [_] (do-my-staff))
 (compile-tags [_] (compile-tags default-compiler))
 (compile-posts [_] (compile-posts default-compiler))
 #_etc...)

There must be a simple way.

3. Multimethods

They are way too flexible, I do not need so much flexibility. But it requires the least ceremony it seems, supporting selective overriding, if we leverage :default and a single other dispatch value:

(defmulti compile-tags-page (constantly :custom))

(defmethod compile-tags-page :default [..] ...)

;; If I want to override it, I just do:
(defmethod compile-tags-page :custom [..] (do-my-custom-stuff))

What do you think? Thanks!

Clojure 1.10 added an :extend-via-metadata flag. You would implement the protocol normally (but with :extend-via-metadata true set) and provide a factory function for an object with the default implementations. Users can override a specific function with

(with-meta compiler {`your-ns/compile-tags-page (fn [_] (do-my-stuff))})
1 Like

Overriding a function to provide extra data? That seems strange? There’s no other way to just pass in more data to the existing function?

The function is an action that produces html from a template and site data such as tags info. It is this fn that passes the data to the template renderer so if I want to pass it more data I must change it. Even if not, I’d need to change another function, that generates the data (based on the site resources). So the need to override stays. But you have a good point, I need to think broader and see whether I can redesign it somehow.

(in practice I actually need to add an extra key to the params map similar to :tags, namely :tag->count close to https://github.com/cryogen-project/cryogen-core/blob/a36e4427a278120634af109b04fd3c0dbd9e59f9/src/cryogen_core/compiler.clj#L491 so I’d perhaps want a way to produce extra params based on existing params and other data available in this fn such as posts-by-tag.)

(For this particular problem I ended up with a different solution but I still thing this question is valid and worth answering?)

Interesting, thank you!

Firstly, there’s nothing wrong with binding. That said, some other ideas include:

  • pass a map whose keys are things like :compile-tags-page and values are the various functions of interest, which would allow a user of the library to merge a map of their own overrides
  • pass a namespace, which can be treated the same way as the above-mentioned map (vars act as key/value pairs)