Sometimes, a function that we are working on needs to take lots of parameters. Not just one or two but a decade or even more. I used to face with such a problem many times. I’m not sure solutions made by me were always good enough.

You might be brave enough to say you will never face such an error. It’s probably a weird architecture. A function should accepts at least five arguments. You will refactor such a code for sure.

But look at this:

    def __init__(self, verbose_name=None, name=None, primary_key=False,
                 max_length=None, unique=False, blank=False, null=False,
                 db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
                 serialize=True, unique_for_date=None, unique_for_month=None,
                 unique_for_year=None, choices=None, help_text='', db_column=None,
                 db_tablespace=None, auto_created=False, validators=(),
                 error_messages=None):

It is from Django, the world-wide spreaded framework to build robust web applications. Except self, the constructor takes 22 optional arguments. Saying more, it is just a basic abstract class. Its descendants require their own arguments in addition to default ones.

Common languages such as Python, Ruby or Java give only one standard way to deal with lots of parameters. In fact, using them you cannot be mistaken since you have no other choice. The question here is how to make you code less ugly than it is now.

People who are new in Clojure, especially if they came from classical Python/Puby/PHP, face troubles when passing lots of arguments into a function. Since it’s Clojure, there are several ways to do that. This article highlights some on them, their benefits and disadvantages.

Multi-arity

Any function in Clojure may have more than one body with its own signature. That’s normal for any functional language, but sounds surprisingly to former Python/Ruby adepts. An interpreter dispatches that bodies by a form of arguments. The first found body is called. When no body is found, an exception is raised.

For example, the same function could be called with either two or three arguments if we declare it in such way:

(defn foo
 ([x y]
  (println "two arguments")
  (+ x y))
 ([x y z]
  (println "three arguments!")
  (+ x y z)))

(foo 1 2)
3 ;; prints `two arguments`

(foo 1 2 3)
6 ;; prints `three arguments!`

Calling a function with zero, one or ten arguments will raise an exception. Depending on your application’s logic, it could be both good or bad behaviour.

To fallback to default body that deals with any set of arguments, add one more implementation:

(defn foo
 ([x y]
   ...
  )
 ([x y z]
  ...
  )
 ([& args]
  (reduce + 0 args)))

Now, you may add any arguments set together:

(foo 1 2 3 4)
10

(foo)
0

The order of bodies is important. If you put [& args] clause on the top, it will cover all the possible function calls. So you will never reach [x y] or [x y z] implementations.

One interesting feature is you may redirect function call inside a body just calling the same function with another argument set. For example:

(defn test
  ([x y]
   (test x y nil)) ;; redirects to the second body
  ([x y z]
   (do-some-stuff x y z)))

Clojure dispatches a proper body quite fast. It is a key feature of Clojure’s runtime. There are lots of core functions that declare two, three or even five bodies regarding to performance issues. It’s much faster then having a single body with multiple ifs, case or condclauses.

A quick copy-paste from Clojure sources:

(defn juxt
  "..."
  ([f] ...)
  ([f g] ...)
  ([f g h] ...)
  ([f g h & fs] ...))

Multi-arity is great when you already have a function that takes a couple of scalar parameters and then you need to add some extra one ASAP. Usually, the most needed function is called in thousand places so adding an extra parameter everywhere would be a mess.

In Java or Python world, it is named “refactoring”. You need a robust commercial IDE to scan the project and change each call of a function. In Clojure, you just do:

;; old
(defn test
  [x y]
  (do-old-stuff x y))

;; new
(defn test
  ([x y]
   (do-old-stuff x y))
  ([x y z]
   (do-new-stuff z)
   (do-old-stuff x y)))

Now you can keep the old calls without changing your code. And pass the new argument only where you need.

Maps

Your function may need lots of additional arguments. For example, boolean flags, extra options for HTTP connection, timeouts, headers, error messages.

A good way to solve the problem is to join them into a single map. Thus, your function accepts only a couple of required parameters and the rest are put into an optional map. Say, you pass a hostname and a method name and a map with :timeout, :headers keys on so on.

Inside a function, you either take optional arguments one by one:

(defn foo [hostname port & [opt]]
  (let [timeout (:timeout opt)
        headers (:headers opt]
    (http-request hostname port timeout headers)
    ...)

or decompose a map on separated variables at once:

(defn foo [hostname port & [opt]]
  (let [{:keys [timeout
                headers]} opt]
    (http-request hostname port timeout headers)
    ...))

Decomposition works as well on the signature level. I do not like this method though since it brings some noise in the code:

(defn foo [hostname port & [{timeout :timeout
                             headers :headers}]]
  (http-request hostname port timeout headers)
  ...)

A good point there is you may keep a default map somewhere and merge it with the passed ones to fallback to default values:


(def foo-defaults
  {:timeout 5
   :headers {:user-agent "Internet Explorer 6.0"}})

(defn foo [hostname port & [opt]]
  (let [{:keys [timeout
                headers]} (merge foo-defaults opt)]
    ;; now timeout is always 5 when not passed
    ...
    ))

This way of passing a map is widely-spreaded across Clojure libraries. It even considered as the standard one because of its simplicity and transparency. If you have just started with Clojure and need a function with multiple arguments, use a map.

Rest arguments as a map

There is another way to deal with multiple arguments. Do you remember the rest arguments prepended with & in a function signature? Something like that:

(defn foo [& args]
  ;; args is a list)

(foo 1 2 3 4) ;; args is (1 2 3 4)

Starting with Clojure 1.5 (or 1.6, I don’t remember exactly) you may turn the rest arguments into a map. The syntax is:

(defn foo [& {:as args}]
  ;; now, args is a map!
  args)

(foo :foo 1 :bar 42)
{:bar 42, :foo 1}

Pass extra arguments remembering some simple rules:

  1. there must be even number of rest arguments, otherwise you will get an error;
  2. each odd argument (usually a keyword) is a key;
  3. each even argument is a value;
  4. you cannot duplicate key items.

Turning rest arguments into a map is also used oftenly in Clojure. You may choose that method over a map as well when developing with Clojure.

Pure map vs rest args

Each of two method described above has its own benefits and disadvantages. Let’s highlight some of them:

  1. Using a map is good when you don’t know exactly what arguments you will pass into a function. It’s a common situation when the final set of options is unknown for the last moment. It might depend on user input, environment variables or any conditions. Usually, you compose a map step by step, validate it with somehow and pass into a function. With sequences, it’s more difficult to compose a set of arguments.

  2. Passing a map into a function with the & rest signature requires some additional steps. You should flatten you map into a vector or sequence and then apply it to a function:

    (def opt {:foo 42 :bar [1 2 3]}) ;; your options map
    
    (defn foo [& {:as args}] ...) ;; but the function accepts rest arguments
    
    ;; turns opt to a seq of (key1 val1 key2 val2)
    ;; then apply it to the function
    (apply foo (mapcat identity opt))
    

    Note how long is the code. Probably, you’d better to modify the function to accept just map.

  3. With the rest arguments, partial works like a charm. Say, we have a function that returns a set of rows from the database. The first argument is a table name, and the rest are a sequence with each odd argument as a column name and even argument as a value:

    (db-query :users :active true :staff true :name "Ivan")
    ;; performs a query like:
    ;; select *
    ;;   from "users"
    ;; where
    ;;   active
    ;;   and staff
    ;;   and name = "Ivan"
    

    Some of our users could be blocked. We may forget passing :active false clause every time you query for users. To prevent returning blocked ones to the frontend, it’s better to have a special function for that:

    (def active-users (partial db-query :users :active true))
    

    In addition to this constraint, we might be interested in only staff users. Let’s extend our function with another partial application:

    (def staff-active-users (partial active-users :staff true))
    

    Finally, we may select all the non-blocked staff users whose name is Ivan:

    (staff-active-users :name "Ivan")
    ;; [{:id 1 :name "Ivan" :surname "Petrov"}
    ;;  {:id 1 :name "Ivan" :surname "Sidorov"}]
    
  4. Instead, with maps you cannot declare a partial function. You need to invent your own partial-map implementation:

    
    ;; let `foo` is a function that accepts a map
    (defn foo [opt]
      opt)
    
    ;; our own `partial`
    (defn partial-map [f defaults]
      (fn [opt]
        (f (merge defaults opt))))
    
    ;; example:
    (def foo-timeout (partial-map foo {:timeout 5}))
    
    (foo-timeout {:bar 42})
    {:bar 42 :timeout 5}
    

Conclusion

The examples above show there are more then one way to deal with multiple arguments in Clojure. These are multi-arity, maps and rest arguments. All of them cover you common requirements as well.

Remember, you are not limited with the only three those ones. With macroses, you can implement you own arguments system: Common Lisp-wise or any other. The only limit there is your imagination.