Groovy Named Arguments are not Arguments

2013-12-08 in groovy java
A fractal of bad implementation

In the past two weeks, we’ve looked at how horribly Groovy handles something as simple as function call arguments. There is yet one more horror Groovy brings to the table: its conception of named arguments.

Named arguments (also known as “keyword arguments”, or just “kwargs”, in some contexts) allow the programmer to specify arguments to a function by name, rather than by position, enhancing readability and in many cases writability, as the meaning of each argument is clarified at use site. It also has the advantage of allowing some arguments to be specified without specifying the preceding ones, in the case of arguments which also have defaults. Examples of languages which support this concept are Python and OCaml.

kwargs.py

 1#! /usr/bin/env python2
 2
 3def example(widget="widget", sprocket="sprocket", xyzzy="nothing happens"):
 4    print("widget=%s, sprocket=%s, xyzzy=%s" % (widget, sprocket, xyzzy))
 5
 6example()
 7# Specifying by position
 8example("foo")
 9example("foo", "bar")
10example("foo", "bar", "plugh")
11# Specifying by name
12example(widget="foo")
13# Can skip sprocket
14example(widget="foo", xyzzy="plugh")
15# Can invert order
16example(xyzzy="plugh", widget="foo")
1widget=widget, sprocket=sprocket, xyzzy=nothing happens
2widget=foo, sprocket=sprocket, xyzzy=nothing happens
3widget=foo, sprocket=bar, xyzzy=nothing happens
4widget=foo, sprocket=bar, xyzzy=plugh
5widget=foo, sprocket=sprocket, xyzzy=nothing happens
6widget=foo, sprocket=sprocket, xyzzy=plugh
7widget=foo, sprocket=sprocket, xyzzy=plugh

There’s a number of important facets of Python’s implementation that make it highly usable for both caller and callee. Most importantly is that names specified by the caller correspond to the names of the arguments in the callee. This means that the callee need not take special care to handle its named arguments — in fact, the callee need not know about named arguments. A corollary is that named arguments play nicely with both optional and mandatory arguments.

Now it’s time to look at how named arguments fit into Groovy’s Lovecraftian function call system. We’ll start with the below program, which at first glance is equivalent to the Python program above.

Kwargs1.groovy

 1package gl.lin;
 2
 3class Kwargs1 {
 4  static example(widget="widget", sprocket="sprocket", xyzzy="nothing happens") {
 5    printf("widget=%s, sprocket=%s, xyzzy=%s\n", widget, sprocket, xyzzy);
 6  }
 7
 8  static main(args) {
 9    example();
10    /* Specifying by position */
11    example("foo");
12    example("foo", "bar");
13    example("foo", "bar", "plugh");
14    /* Specifying by name... right? */
15    example(widget: "foo");
16    example(widget: "foo", xyzzy: "plugh");
17    example(xyzzy: "plugh", widget: "foo");
18  }
19}

Of course, if you actually thought that that would be equivalent to the Python program, your feelings toward Groovy are still all too high. The output we actually get is

1widget=widget, sprocket=sprocket, xyzzy=nothing happens
2widget=foo, sprocket=sprocket, xyzzy=nothing happens
3widget=foo, sprocket=bar, xyzzy=nothing happens
4widget=foo, sprocket=bar, xyzzy=plugh
5widget={widget=foo}, sprocket=sprocket, xyzzy=nothing happens
6widget={widget=foo, xyzzy=plugh}, sprocket=sprocket, xyzzy=nothing happens
7widget={xyzzy=plugh, widget=foo}, sprocket=sprocket, xyzzy=nothing happens

Map named arguments to the corresponding parameters? Bah, we’ll just pack them into an untyped map, programmers love untyped maps. More specifically, Groovy’s implementation of named parameters is to create a map (a LinkedHashMap, specifically) containing the named arguments given, using string values for the name of each parameter. This map is then passed in as one of the arguments. There are many reasons this is terrible for the caller, but let’s look at the callee for a moment. Unlike in Python, a Groovy function which takes named arguments must be prepared to take named arguments, and manually extract arguments out of the map which Groovy provides it.

Where does the map of named parameters get passed, you ask? At the end of the argument list, corresponding to where callers conventionally write their named arguments?

Kwargs2.groovy

 1package gl.lin;
 2
 3class Kwargs2 {
 4  static example(widget="widget", sprocket="sprocket", xyzzy="nothing happens") {
 5    printf("widget=%s, sprocket=%s, xyzzy=%s\n", widget, sprocket, xyzzy);
 6  }
 7
 8  static main(args) {
 9    example("widget", "sprocket", xyzzy: "plugh");
10  }
11}
1widget={xyzzy=plugh}, sprocket=widget, xyzzy=sprocket

Nope, the named parameter map comes at the beginning of the arguments, displacing the arguments written before it in the caller. It is hard to express how wrong this is. The main issue is that it creates scenarios in which the caller has no choice but to pass at least one named parameter, simply so it can call the function. The callee can partially work around this by making its first parameter optional, defaulting to an empty map. But since optional parameters must be specified left-to-right, the situation crops up again if the callee wishes to have any other optional arguments.

KwargsOpt.groovy

 1package gl.lin;
 2
 3class KwargsOpt {
 4  static example(kwargs = [:], mandatory, optional = "foo") {
 5    printf("mandatory=%s, optional=%s, kwargs=%s\n",    \
 6           mandatory, optional, kwargs);
 7  }
 8
 9  static main(args) {
10    /* Calling with mandatory argument only -- works fine */
11    example("mandatory");
12    /* Trying to specify optional --- gets passed into kwargs */
13    example("mandatory", "optional");
14    /* Need to specify at least one named argument to pass anything into
15     * optional */
16    example("mandatory", "optional", dummy: null);
17  }
18}
1mandatory=mandatory, optional=foo, kwargs={}
2mandatory=optional, optional=foo, kwargs=mandatory
3mandatory=mandatory, optional=optional, kwargs={dummy=null}

Notice, in particular, how absurd the second case really is. Because of how Groovy’s optional parameters work, the arguments list effectively got shifted to the left by one space.

This case aside, however, Groovy’s implementation has severe negative consequences for the caller, in addition to those on the callee. Because the parameter names become strings that don’t need to match anything in particular, the caller has no assurance that it is actually calling the callee in a meaningful way. If the programmer makes a typo in one of the names, Groovy happily passes the parameter in with the incorrect name, which almost always results in the callee silently ignoring it — after all, manually checking whether every parameter in the named-parameters-map argument is actually understood would be a substantial amount of boiler-plate and entails more work than the programmer can save via the readability and flexibility of named arguments.

There’s also the issue of type safety. Since the named parameters map isn’t a map of associated data, but a heterogenous dynamic structure, the caller can’t reason about the types that any of its named parameters actually have. The most specific type that can be given to the named-parameters-map argument would be Map<String,Object>, which reduces to Map anyway because Groovy doesn’t really support generics.

Overall, a callee which takes named parameters need to go through the trouble of supporting them, explicitly defaulting both the map parameter and the named parameters themselves, and explicitly extracting the named parameters. The callee cannot have optional arguments without making at least one named parameter mandatory. Neither the caller nor the callee has any assurance, be it compile-time or run-time, that the named parameters have any semblance of correctness, neither by type nor even by name — a class of silent problems normally only encountered in the world of PHP. When someone uses named parameters in Groovy, everyone loses — indeed, I have never seen anyone try to use this feature more than once.