Clojure: strategy for adding different arity to existing function

On an established codebase I have a function with a signature like so:

(defn myfn [kw &REST] )

This has survived multiple revisions where another optional arg or two end up added to &REST. We’ve hit the point where it would be better to start using a map of args instead of sequential, to support adding new features. So the new function should be:

(defn myfn [kw optmap] )

However, for reverse-compatibility reasons, I need to maintain support for the original signature as well. I can think of several ways to accomplish this:

  1. Make myfn a multimethod with dispatch on the type of the second arg
  2. Check type within the fn itself
  3. Generate a new method with a different name with the latter signature.

Number 1 seems “correct”, but produces the most code.

Number 2 is the simplest to implement but makes an ugly function.

Number 3 will cause a division in old code and new code (causing vestigial traces of when things were written), but seems in line with the talks against semantic versioning (just make a new thing to reduce ambiguity).

I know this is a common enough problem; what solutions have you chosen, why, and have you had any regrets?

1 Like

IMO it’s a new function name, and one can wrap the other.

E.g. the [kw & rest] version can simply wrap the new keywords version. That way you maintain only one implementation, two names with two signatures.

You can even mark the old one as ^:deprecated, which might show up in your IDE / compilation step (if you’re using CLJS)

4 Likes

I didn’t know that about ^:deprecated - very cool

1 Like

Number 1 and Number 2 are in essence the same. Being Multimethods the framework for the pattern you would recognize if you repeat Number 2 many times.

1 Like

If you’re talking about optional named (keyword) arguments in the legacy code, consider that you will have either one argument (and no options), or 3, 5, 7, … arguments if keyword arguments are provided.

The new arity will have precisely two arguments. They don’t overlap.

You might want the 1-arity left as a convenience for an empty map of arguments.

So you can do the following:

(defn myfn
  ([kw] (myfn kw {}))
  ([kw optmap] ... the new implementation ...)
  ([kw arg & more] (myfn kw (apply hash-map arg more))))

This maintains the original calling sequence and the new one:

(myfn :x) ; no options
(myfn :y {:a 1 :b 2}) ; options :a and :b -- new, preferred calling sequence
(myfn :z :a 1) ; (myfn :z {:a 1})
(myfn :q :a 1 :b 2) ; (myfn :q {:a 1 :b 2})

This was the approach I took when migrating clojure.java.jdbc from the old, named argument arities to the new option-map arities.

3 Likes

This is what Rich means when he talks about extensible dynamic systems, I think. No type declarations were killed in the making of this function.

Even if you were to have conflicting arities (old 3-ary conflicting with new system), you could do a check in the beginning and see if you got a keyword (multi-arity) or a map (new system).

1 Like