Kwargs vs explicit parameter map for APIs?

882 views
Skip to first unread message

Colin Fleming

unread,
Apr 25, 2014, 6:41:22 PM4/25/14
to clo...@googlegroups.com
Hi all,

I'm working on an API at the moment, and I'm balancing whether to use inline keyword args which I would destructure in the functions, or whether to just pass an explicit params map as the last parameter. Comparison of the two options in case I'm not explaining myself well:

Kwargs:
(class/create-class :instance    list
                    :description "My description"
                    :implements  (keys class-methods)
                    :methods     (calculate-my-methods))

Map:
(class/create-class {:instance    list
                     :description "My description"
                     :implements  (keys class-methods)
                     :methods     (calculate-my-methods)})

A lot of APIs I've seen have favoured kwargs, and it undeniably makes for some pretty code - Seesaw is the best example I've seen here, the API is a thing of beauty. However it seems to me to have some issues:
  1. If I want to delegate to another call from within an API function and use the same arguments, it's really awkward: (apply delegate (mapcat identity args)) or some similarly awful black juxt magic. Or of course writing out all the parameters again, but that's even worse.
  2. It's more difficult to make parameters optional based on some runtime criteria since the params are baked into the function call. I guess this is usually dealt with by making the calls handle nil for a particular parameter.

Both of these are much easier when passing an explicit map. Any preferences here, from either writing or using APIs like this?

Cheers,

Colin

Andrey Antukh

unread,
Apr 25, 2014, 6:56:32 PM4/25/14
to clo...@googlegroups.com
Hi! 

I have the same doubt!

However, At this time, I prefer use a explicit map instead keywords, because for me is much clear that using keywords.

Andrey.


--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--

Dave Ray

unread,
Apr 25, 2014, 6:58:49 PM4/25/14
to clo...@googlegroups.com
Seesaw looks nice, but in retrospect I would use explicit maps if I had it to do all over again for exactly the reasons you mention. These days, I always use explicit maps for options.

Dave

Gary Trakhman

unread,
Apr 25, 2014, 6:59:03 PM4/25/14
to clo...@googlegroups.com
If it's more than a few parameters, I prefer maps..  It enables the possibility of things like merge.

Colin Fleming

unread,
Apr 26, 2014, 4:33:45 AM4/26/14
to clo...@googlegroups.com
Thanks everyone - I was leaning towards maps and sounds like that's the preferred approach.

Glen Mailer

unread,
Apr 26, 2014, 6:03:26 AM4/26/14
to clo...@googlegroups.com
While reading the clojure.java.jdbc code yesterday, I discovered to my surprise that map destructuring can be used after an & in an arglist.

This should give you all the described benefits of the two approaches you mention

((fn [a & {:as d}] d) 1 :opt1 'blah :opt2 false)

You can also do defaults using :or - although these defaults don't get merged into the value of :as - so an explicit (merge) might be better.

((fn [a & {:keys [misc] :or {misc :misc-default} :as d}] [d misc]) 1 :opt1 'blah :opt2 false)

Hope that makes sense!

Glen

Colin Fleming

unread,
Apr 26, 2014, 6:42:56 AM4/26/14
to clo...@googlegroups.com
Yes, that's the preferred way of accepting kwargs now, I think. But it's just prettier syntax around the same problem. Note that using this form doesn't accept an explicit map, I still have to place the params inline in the function invocation. If I have:

(defn test1 [& {:keys [param1 param2] :as args}]
  ...)

(defn test2 [& {:keys [param1 param2] :as args}]
  ...)

Then when I call, say, test1, I still do it like this:

(test1 :param1 "some value" :param2 "some value")

Which means that if I want to have param2 be optional based on some runtime value, there's really no other way to do it than:

(if condition?
  (test1 :param1 "..." :param2 "...")
  (test1 :param1 "..."))

With two parameters that might be ok, but with 10 it's basically impossible, and your only option is to pass nil for some parameters and handle that inside your function. And if I want to call test1 from test2 I have to do either:

(defn test2 [& {:keys [param1 param2] :as args}]
  (test1 :param1 param1 :param2 param2))

or:

(defn test2 [& {:keys [param1 param2] :as args}]
  (apply test1 (mapcat identity args)))

The first again doesn't scale past 2 or 3 params, and doesn't allow you to deal with optional parameters. The second probably needs a bit of head scratching to figure out what it's even doing the first time you see it.

Note that if I pass an explicit map, I can still use the destructuring forms above, just without the & symbol, and then I can just pass the args map around, merge it for default parameters, add/remove params etc, and everything is good.

Cheers,
Colin


--

Sean Corfield

unread,
Apr 29, 2014, 10:54:12 PM4/29/14
to clo...@googlegroups.com
Having worked through this for java.jdbc (and to a lesser extent for CongoMongo), I think that I would approach it slightly differently in future:

Provide two functions: one taking a map, one taking keyword arguments; the latter delegating to the former via & {:as opts} and then internal calls can easily use the map-based argument approach.

One of the additional complexities in java.jdbc (as it stands today) is that several functions accept an arbitrary number of arguments, followed by optional keyword arguments as well, so several functions have to parse their argument lists at runtime. Simplifying that to use a sequence (of current arguments) followed by a map (of options) would make several things easier inside the library, removing a lot of apply calls, as well as providing an API that is more amenable to dynamic call construction...

I still think the keyword argument approach is far more readable to _users_ - and it's also what was recommended in the Clojure Library Guidelines right up until a few days ago:


Reid McKenzie changed it on April 24th, completely reversing the previous recommendation from "Unroll optional named arguments" to "Don't unroll optional named arguments" - compare version 6 (from Stuart Halloway) with Reid's edits in versions 7-11:


Sean

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Sean Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)



signature.asc

James Reeves

unread,
Apr 29, 2014, 11:41:29 PM4/29/14
to clo...@googlegroups.com
On 30 April 2014 03:54, Sean Corfield <se...@corfield.org> wrote:
I still think the keyword argument approach is far more readable to _users_

Really? It's only an omission of two braces. While readability is subjective, I'm not sure how that can be considered to be far more readable.

From a purely practical perspective, explicit maps are generally a lot easier to work with. To my mind the disadvantages of keyword arguments often outweigh the small presentational benefits they have.

- James

Alex Baranosky

unread,
Apr 30, 2014, 1:07:51 AM4/30/14
to clo...@googlegroups.com
I'm extremely internally torn regarding kwargs.  I use them a lot; I know they hinder composability; but every time I go back to straight maps for these kinds of things I really don't like all the extra noise characters and go back to kwargs.

I feel like I really should be using regular maps for all the good reasons listed, but in practice I don't. Honestly, I don't usually run into the composability issues too often, so I'm getting by "ok" with my indulgence in kwargs, I suppose.

I especially dislike that my non-kwarg fns no-longer can elegantly accept no options.  Let me illustrate:

(defn foo [& {:keys [a b]}]
  [a b])

(foo :a 1 :b 2)
(foo)

(defn foo [{:keys [a b]}]
  [a b])

(foo {:a 1 :b 1})
(foo {}) ;; <-- unpleasant to my eyes, let me reimplement foo

(defn foo
  "This is also pretty gross"
  ([]
     (foo {}))
  ([{:keys [a b]}]
     [a b]))

So, yeah, I'm pretty torn.

I like the option of having kwargs for the outer API shell, and explicit option maps internally.  I've played with that a little as well at work to some "ok" success.

Alex



--

James Reeves

unread,
Apr 30, 2014, 1:55:29 AM4/30/14
to clo...@googlegroups.com

On 30 April 2014 06:07, Alex Baranosky <alexander...@gmail.com> wrote:
I especially dislike that my non-kwarg fns no-longer can elegantly accept no options.  Let me illustrate:

(defn foo [& {:keys [a b]}]
  [a b])

(foo :a 1 :b 2)
(foo)

You could write:

(defn foo [& [{:keys [a b]}]]
  [a b])

(foo {:a 1 :b 2})
(foo)

- James

Colin Fleming

unread,
Apr 30, 2014, 3:04:31 AM4/30/14
to clo...@googlegroups.com
After posting this last week I decided to go down the explicit map route, and I'm already really glad I did. Amongst other things, it has allowed me to have two similar calls easily share their common options and merge in the differences. I'm generally a fan of maintaining fairly minimal APIs, I don't think I'll be duplicating functions, I don't really see a need - two extra braces are just not that much noise for me. I'd rather use Alex's last example with the optional map arg than have my-function and my-function-from-map everywhere.


--

Alex Baranosky

unread,
Apr 30, 2014, 3:59:58 AM4/30/14
to clo...@googlegroups.com
Thanks James, that's a useful version to experiment with :)

Joachim De Beule

unread,
Apr 30, 2014, 5:03:11 AM4/30/14
to clo...@googlegroups.com, ja...@booleanknot.com
my two cents:

The extra readability to users when using keyword args also comes from the fact that a function's options are explicit in its signature. So during development, instead of having to look them up in the docs or in the code, my emacs mini-buffer simply shows them to me. Although I do agree with all the good reasons against keywords arguments, to me this is still the decisive reason to prefer them...

Joachim

Op woensdag 30 april 2014 05:41:29 UTC+2 schreef James Reeves:

Colin Fleming

unread,
Apr 30, 2014, 6:11:18 AM4/30/14
to clo...@googlegroups.com, ja...@booleanknot.com
But that's only true for the variables which are explicitly destructured in the function definition, which in my experience many are not - they're often later picked out of an ":as args" argument, perhaps dependent on a combination of the parameters which are explicitly destructured. Seesaw never does this, for example. I think it's dangerous to rely on this rather than the documentation since it's often an incomplete view of what the function requires.

Cheers,
Colin


Colin Fleming

unread,
Apr 30, 2014, 6:14:35 AM4/30/14
to clo...@googlegroups.com
And thinking about it (after pressing "send", of course), you'd get the same benefit from destructuring an explicit map in the function parameter anyway, wouldn't you?

Jim Crossley

unread,
Apr 30, 2014, 11:03:03 AM4/30/14
to clo...@googlegroups.com
We used kwargs for options extensively in Immutant 1.x, and we're moving to explicit maps for Immutant 2.x, for the reasons cited above.

It's not obvious to me why the "bad" release-sharks example on the coding standards page [1] is bad. Why should the optional config be the "least variance argument"?

I had to look up "laudable", btw. It's one of those good words that sounds bad. :)


Alex Robbins

unread,
Apr 30, 2014, 11:18:35 AM4/30/14
to clo...@googlegroups.com
Maybe they want config in the least variance argument so it can be partial'ed?

Jim Crossley

unread,
Apr 30, 2014, 2:25:53 PM4/30/14
to clo...@googlegroups.com
Granted, but the word "bad" seems harsh without any explanation, especially if the common usage is to pass an empty map.

Sean Corfield

unread,
Apr 30, 2014, 5:53:24 PM4/30/14
to clo...@googlegroups.com, Stuart Halloway
On Apr 30, 2014, at 8:03 AM, Jim Crossley <j...@crossleys.org> wrote:
It's not obvious to me why the "bad" release-sharks example on the coding standards page [1] is bad. Why should the optional config be the "least variance argument"?

I had to look up "laudable", btw. It's one of those good words that sounds bad. :)


Well, that's a very recent change. Stuart Halloway's version has been the standard for years. Reid made those changes only a few days ago - and I saw no discussion of the proposed changes so I'd like to hear from Cognitect's folks about this: is it a change of heart by Clojure/core or are they unaware of the change?
signature.asc

Gary Trakhman

unread,
Apr 30, 2014, 6:02:13 PM4/30/14
to clo...@googlegroups.com, Stuart Halloway
I think they're unaware of the change, as it resulted from a recent conversation on IRC that same day, where sentiment indicated that kwargs are generally more trouble than they're worth and there's still confusion around it.

What started it: the example of keys-destructuring on a list in a let binding, very odd to explain.
> (let [{:keys [a b c]} '(:a 1 :b 2 :c 3)] 
    [a b c])

[1 2 3]



Mark Engelberg

unread,
Apr 30, 2014, 8:06:53 PM4/30/14
to clojure
Here's the thing I can't stand about keyword args:

Let's start off with a simple function that looks for keys x and y, and if either is missing,
replaces the value with 1 or 2 respectively.

(defn f [& {:keys [x y] :or {x 1 y 2}}]
  [x y])

=> (f :x 10)
[10 2]

So far, so good.

Now, let's do an extremely simple test of composability.  Let's define a function g that destructures the keyword args, and if a certain keyword :call-f is set, then we're just going to turn around and call f, passing all the keyword args along to f.

(defn g [& {call-f :call-f :as m}]
  (when call-f
    (apply f m)))

=> (g :call-f true :x 10)
[1 2]

What?  Oh right, you can't apply the function f to the map m.  This doesn't work.  If we want to "apply" f, we somehow need to apply it to a sequence of alternating keys and values, not a map.

Take 2:

(defn g [& {:keys [call-f x y] :as m}]
  (when call-f
    (f :x x :y y)))

OK, so this time we try to workaround things by explicitly calling out the names of all the keywords we want to capture and pass along.  It's ugly, and doesn't seem to scale well to situations where you have an unknown but at first glance, it seems to work:

=> (g :call-f true :x 80 :y 20)
[80 20]

Or does it?

=> (g :call-f true :x 10)
[10 nil]

What is going on here?  Why is the answer coming out that :y is nil, when function f explicitly uses :or to have :y default to 2?

The answer is that :or doesn't do what you think it does.  The word "or" implies that it substitutes the default value of :y any time the destructured :y is nil or false.  But that's not how it really works.  It doesn't destructure and then test against nil; instead the :or map only kicks in when :y is actually missing as a key of the map.

This means that in g, when we actively destructured :y, it got set to a nil, and then that got passed along to f.  f's :or map didn't kick in because :y was set to nil, not absent.

This is awful.  You can't pass through keyword arguments to other functions without explicitly destructuring them, and if you destructure them and pass them along explicitly, nil values aren't picked up as absent values, so the :or default maps don't work properly.

To put it simply, keyword args are bad news for composability.

It's a shame, and I'd love to see this improved (rather than just having the community give up on keyword arguments).

Jim Crossley

unread,
Apr 30, 2014, 11:04:59 PM4/30/14
to clo...@googlegroups.com
Unless I'm missing something subtle, all of your points would hold if you removed the & in your argument vector to turn your kwargs into an explicit map, wouldn't they? One advantage is you'd be able to (apply f [m]), but I'm not sure the :or logic would be any less troublesome.


--

Colin Fleming

unread,
Apr 30, 2014, 11:15:50 PM4/30/14
to clo...@googlegroups.com
I think it would because in that case you'd just pass your arg map straight through rather than having to reconstruct it. So if you weren't passed :y in g (in Mark's example), g wouldn't pass it on to f. By forcing the reconstruction of the map from explicit args, you're forced to use the value (incorrectly) destructured in g. Mark could work around it in his example by using (apply f (mapcat identity m)) in g, but it's far from intuitive.

Jim Crossley

unread,
May 1, 2014, 12:21:03 AM5/1/14
to clo...@googlegroups.com
Oh, right. (f m) instead of (apply f [m]). Duh. 

Dylan Butman

unread,
Mar 13, 2015, 1:46:08 PM3/13/15
to clo...@googlegroups.com, j...@crossleys.org
Late chime in...how about both?

(defn kargs
  ([] (kargs {}))
  ([a b & {:as r}]
   (kargs (assoc r a b)))
  ([a] a))

Dylan Butman

unread,
Mar 13, 2015, 1:58:19 PM3/13/15
to clo...@googlegroups.com, j...@crossleys.org
or a macro

(defmacro defkargs [name destrt & body]
  `(clojure.core/defn ~name
     ([] (~name {}))
     ([a# b# & {:as r#}]
      (~name (clojure.core/assoc r# a# b#)))
     ([~destrt]
      ~@body)))

(defkargs kargs-test {:keys [some me]}
  (when (and some me)
    (+ some me)))

(and (= (kargs-test :some 1 :me 2)
        (kargs-test {:some 1 :me 2})
        3)
     (= (kargs-test)
        nil))

Leon Grapenthin

unread,
Mar 16, 2015, 5:42:37 PM3/16/15
to clo...@googlegroups.com
Kwargs has clearly been designed for one purpose: A caller should have to type less. 

A simple rule to follow is to use kw args if the exposed thing is a function not expected to be used in functional composition or a certain DSLish kind of macro. 

If your exposed function will be used in functional composition more often than called in typed out code, with kwargs you are using the feature to its opposite purpose: People have to type even more.

For an example, if your thing is called "load-config-file!" and is used in one or two places of code, use kwargs by all means. If your thing is called path-for and resolves an URL for a map of parameters, kwargs is a very unfortunate choice.

tcrayford

unread,
Mar 23, 2015, 5:55:39 AM3/23/15
to clo...@googlegroups.com
Hi there,

I feel pretty strongly about this - I *much* prefer using APIs with explicit options maps. The community is pretty divided though. I wrote up the tradeoffs (which are well discussed here as well), as well as a list of libraries using each style: http://yellerapp.com/posts/2015-03-22-the-trouble-with-kwargs.html

To me, typing two extra characters ain't a big deal, but unrolling/rolling up maps and not being able to manipulate options easily is a bunch of pain, so I always choose explicit maps wherever possible.

Colin Yates

unread,
Mar 23, 2015, 6:07:42 AM3/23/15
to clo...@googlegroups.com
If there are enough optional args that they need to be named then I
choose a map, every time. That said, I do must multi-arity fns quite a
bunch as well (with the options map going on the end if applicable).

I also depend quite heavily on prismatic's schema, and to a lesser
extent core.typed and I am not sure how charges fit in with either of
those, although I haven't tried.

+1 for maps.

Mark Engelberg

unread,
Mar 23, 2015, 6:13:55 AM3/23/15
to clojure
Your article makes things a little more complicated than they need to be -- there is a special destructuring syntax in Clojure for pouring keyword args directly into a map (just do the map destructuring after the &).  Also, I don't think you need to flatten the map of keyword args before calling "apply".  It's been a while since I've tested this, but I believe that if a map is passed as the last argument to apply, Clojure "does the right thing" and passes the map in as keyword args.

Despite that, I agree with you that keyword arg functions don't compose as well.
If I'm remembering correctly, I think the biggest problem I ran into is that the :or destructuring technique for supplying default values only uses the default values if the specified keys are missing from the keyword args (as opposed to using the default values if the keyword args are set to nil).  This became a problem when I'd destructure in one function (which would bind missing keys to nil) and then pass along the keywords to the next function.  The missing keywords weren't treated as missing, and so the default values weren't supplied.

It's been a while since I've needed to do this, so I apologize in advance if I'm inadvertently passing along some misinformation here.


--

Mark Engelberg

unread,
Mar 23, 2015, 6:41:38 AM3/23/15
to clojure
Ha! I just noticed that this thread was started a year ago, and I posted a similar comment back then.  Well, at least I'm consistent :)


Matching Socks

unread,
Mar 23, 2015, 7:36:00 PM3/23/15
to clo...@googlegroups.com
Could you give an example demonstrating this?

user> (defn a [& {x :x y :y}] (vector x y))
#'user/a

user
> (a :y 7 :x 3)
[3 7]

user> (apply a {:x 3 :y 7})
[nil nil]

James Reeves

unread,
Mar 23, 2015, 8:15:04 PM3/23/15
to clo...@googlegroups.com
On 23 March 2015 at 10:13, Mark Engelberg <mark.en...@gmail.com> wrote:
It's been a while since I've tested this, but I believe that if a map is passed as the last argument to apply, Clojure "does the right thing" and passes the map in as keyword args.

I'm afraid not. Not even in the latest Clojure 1.7.0 alpha.

- James

Mark Engelberg

unread,
Mar 23, 2015, 8:16:23 PM3/23/15
to clojure
Yes, I thought that technique worked, but I just conducted the same experiment as you did and confirmed that it doesn't.  I must have been misremembering.  You are correct that passing a map to a keyword-arg function is more of a nuisance than it should be.

Joseph Smith

unread,
Mar 23, 2015, 8:25:51 PM3/23/15
to clo...@googlegroups.com
I believe the current behavior with apply is the correct behavior as a map can be treated as a sequence of "MapEntry"s. 

--

Marcus Magnusson

unread,
Mar 24, 2015, 6:03:17 PM3/24/15
to clo...@googlegroups.com
How come this is so debated, but normal variadic functions are not? Is it only because there's nothing equivalent to apply for kwarg functions? Would it really be such a huge addition to clojure.core to add a simple mapply function, especially given that the core API have kwarg functions? (this might not be the right forum to discuss language additions, but might be interesting to see if anyone else agrees)

Leon Grapenthin

unread,
Mar 24, 2015, 8:31:01 PM3/24/15
to clo...@googlegroups.com
I guess with "mapply" using kwarg defined functions in compositional context would be helpful.

It would have to be implemented as a method of the function object itself so that the map can be passed directly without any transformation.

n aipmoro

unread,
Apr 5, 2015, 7:30:03 PM4/5/15
to clo...@googlegroups.com
In the blog post, tcrayford gives an example of the difficulties in wrapping a kwargs function:

(defn my-wrapper [& kwargs]
  (let [options (assoc (apply hash-map kwargs) :my-default-arg 1)]
    (apply (your-api-fn (flatten (into '() options))))))

Sure, that's convoluted (and I couldn't actually get it to work on real code).
But isn't this a much simpler way:

(defn my-wrapper [& kwargs]
  (apply your-api-fn :my-default-arg 1 kwargs))

Am I overlooking something?

--

tcrayford

unread,
Apr 5, 2015, 7:46:57 PM4/5/15
to clo...@googlegroups.com
Yep: that won't override the arg if the caller passes it to you as well.

n aipmoro

unread,
Apr 5, 2015, 9:06:06 PM4/5/15
to clo...@googlegroups.com
That explains it. In the type of stuff I write, that possibility wouldn't arise:
there is no calling code that I can't control, and I definitely want the ability
to change my wrapper's default.

In any case, I realized the reason your blog code didn't run was an error:
you need to change (apply (your-api-fn ... to (apply your-api-fn ...

On Sun, Apr 5, 2015 at 7:46 PM, tcrayford <tcra...@gmail.com> wrote:
Yep: that won't override the arg if the caller passes it to you as well.
Reply all
Reply to author
Forward
0 new messages