Monday, September 23, 2013

Named Parameters for Clojure

Clojure can simulate named parameters, or a mix of positional and named parameters. This is really old news, dating back to Clojure 1.2 if evidence serves, but I always have trouble finding this exact syntax, and it is not fully obvious.

Say you want to accept a certain number of optional parameters with names, and perhaps, defaults. It turns out you can combine rest parameters (the ones that come after a &) with map destructuring.

In the simple case, you don't care what the possible options are, and you don't have any defaults.

The keys and values you pass to this function, say (named-parameters :foo 1 :bar 2), are collected together as symbol params.

If you don't provide an even number of values (that is, the same number of keys and values), you'll get a reasonable exception, such as java.lang.IllegalArgumentException: No value supplied for key: :bar

Easy-peasey ... but you need to extract values from the params map to use them inside the function, e.g.: (:foo params). It would be nicer to have them as symbols, just like with normal positional parameters. This is also easy, by leveraging more of the features of map destructuring:

The :keys identifies the keywords expected in the map; it works backwards from the symbol name, foo, to the expected keyword, :foo.

There's also a :syms (for when the keys are expected to be symbols) and :strs (for when the keys are expected to be strings).

The :or identifies default values for each symbol. The end result is that we can rely on defaults from :or or provide our own values when invoking the function:

And since this is Clojure, you can combine all of these things together quite easily ... some positional parameters, some named, some identified by keywords, others identified by symbols.

4 comments:

  1. Prismatic has a good impl of this in their plumbing library as defnk

    ReplyDelete
  2. Unfortunately, named parameters don't always compose well. If you call from one function with optional args to another, the :or map doesn't fill in the missing values.

    (defn named-params
    [& {:keys [foo bar]
    :or {foo "foo-default" bar "bar-default"}}]
    {:output-foo foo :output-bar bar})

    (defn named-params2
    [& {:keys [foo bar]}]
    (named-params :foo foo :bar bar))

    => (named-params2)
    {:output-foo nil, :output-bar nil}

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. You say the "second function should dispatch more intelligently". But it is quite difficult to achieve the semantically desired behavior, and that's precisely the point.

    The crux of the problem is that `nil` is used to represent the absence of an optional input, but when you pass this along, the receiving function doesn't interpret the `nil` as the absence of an input. In fact, there is no good way to pass in any value that represents the input is missing, other than simply not passing in the input. So there's no convenient way to bundle up a bunch of possibly present and possibly missing values and pass them along to another function in a way where it can recognize which were present and which were missing. Taking cases on every nil value and making different dispatch calls would just be a crazy way to do it.

    ReplyDelete

Please note that this is not a support forum for Tapestry. Requests for help will be deleted. Please subscribe to the Tapestry user mailing list if you are in need of support, or contact me directly for professional (for pay) support.

Spammers: Don't bother. I delete your comments and it's a waste of time for both of us. 垃圾邮件发送者:不要打扰。我删除您的评论和它的时间对我们双方的浪费