Pretending Clojure Has Argument Defaults

Although Clojure doesn’t have the ability to specify the default value of a function argument, it is common to use multiple function signatures to achieve serve the same purpose. We can also “destructure” a variadic function (function is arbitrary number of arguments) to get default argument values.

Simple Example

A great example of having default values for function arguments is the range. Of course, this function is built into Clojure but it makes a great demonstration of default argument values.

Below is an implementation of a function that has a similar behavior as range, except it returns a vector. Let’s call our function my-range and write it to take the same arguments as range.

(defn my-range
  [start end step]
  (loop [i start
         result []]
    (if (> i end)
      (recur (+ i step)
             (conj result i)))))

Let’s compare our my-range function to Clojure’s range function.

(my-range 0 5 1) ; => [0 1 2 3 4]
(range 5)        ; => (0 1 2 3 4)

Wouldn’t it be nice if we could call my-range with only 1 or 2 arguments, just like range? The range function assumes that start is 0 and step is 1 unless otherwise specified. Lets implement that:

(defn my-range-2
   (my-range-2 0 end))
  ([start end]
   (my-range-2 start end 1))
  ([start end step]
   (loop [i start
          result []]
     (if (>= i end)
       (recur (+ i step)
              (conj result i))))))

(my-range 5)    ; => [0 1 2 3 4]
(my-range 5 10) ; => [5 6 7 8 9]

When our functions have only 1 signature, we list our arguments in []s directly after the function name. When we want to write a function with multiple signatures we put multiple sets of []s, each in their own () expressions. Using this, we can write a single function that has a different behavior based on the number of arguments passed.

Notice how the two branches of my-range-2 that don’t have all 3 arguments simply call my-range-2 with the default argument values we wanted?

Stuart Sierra has a great demonstration here of this concept used more generally in the context of optional arguments.

In other words, when we call my-range-2 without all three arguments, the my-range-2 function calls itself with all missing arguments set to their default values.

Can we do better?

What if we wanted to specify a value for end and step but use the default value for start. Not even Clojure’s range supports this.

We can do it. But should we? I rarely stop to ask myself that question until its too late, so let’s implement it.

(defn my-range-3
  [& {:keys [start end step] :or {start 0 step 1}}]
  (loop [i start
          result []]
     (if (>= i end)
       (recur (+ i step)
              (conj result i)))))

(my-range-3 :step 2 :end 10) ; => [0 2 4 6 8]

This changes the way which we have to call our function, but it works!

In Clojure, we can pack an arbitrary number of arguments into a single collection that is given one name using the & symbol. This is called a variadic function, apparently. I didn’t actually know that until I looked it up to write this post. For example:

(defn foo
    [x & args]
    (apply str args))
(foo "I" "Have" "So" "Many" "Arguments") ; => "HaveSoManyArguments"

Notice that "I" is bound to x so when we (apply str args) it is not included in the result.

We can “destructure” the arbitrarily long collection of arguments and bind each value to a symbol, such as start, end, or step. When destructuring, you can also specify default values to be used when a key isn’t present in the collection of arguments.

For a better explanation of destructuring, see Clojure’s destructuring guide.

A bonus feature of this approach is that you can set arguments in any order you want. Notice that in our call to my-range-3 we have set the step before we set the end, which is not possible using Clojure range function.

Let’s revisit the question of should you implement your function in the way we implemented my-range-3. I suppose this would come down to personal preference, but my recommendation is that you avoid destructuring a variadic function in order to get default values for your function’s arguments.

I am going to echo Stuart Sierra’s warning while we consider this example:

(my-range-3 :step 2 :end 10 :reverse true) ; => [0 2 4 6 8]

But our my-range-3 function doesn’t support a :reverse argument! Those using our code will be woefully mislead when they accidentally pass the incorrect amount of arguments yet the function doesn’t throw an error or change behavior.

I personally find destructuring to be much harder to read, compared to multiple function signatures.

I also personally find the way destructuring changes the function call to be harder to read. Sure, Clojure users are familiar with alternating key-value arguments, so maybe that doesn’t bother you.

As far as performance, I have not thoroughly tested the difference between my-range-2 and my-range-3. I will say that both seemed to perform similarly when given just the end argument, but my-range-2 started outperforming my-range-3 once all 3 arguments were specified.

(time (repeat 10000 (my-range-2 0 100 10)))
; "Elapsed time: 0.047652 msecs"

(time (repeat 10000 (my-range-3 :start 0 :step 10 :end 100)))
; "Elapsed time: 0.083339 msecs"

If anyone has any other ways to implement default argument values in Clojure, I look forward to reading about them in the comments! Got a useful improvement to the information here? Be sure to let me know!