Groovy Optional Arguments are Unusable
As mentioned last week, one of the main reasons Java has method overloading was
to emulate optional arguments, albeit verbosely. In the below example, our
multiple definitions of doSomething
allow it to be called with any prefix of
its argument list, the other parameters “defaulting” to something that the
implementor considered reasonable.
DoSomething.java
1package gl.lin;
2
3public class DoSomething {
4 public static void doSomething(String a, String b[], int n) {
5 /* Lots of code here */
6 }
7
8 public static void doSomething() {
9 doSomething("");
10 }
11
12 public static void doSomething(String a) {
13 doSomething(a, new String[0], 0);
14 }
15
16 public static void doSomething(String a, String b[]) {
17 doSomething(a, b, b.length);
18 }
19}
C++, on the other hand, allows the programmer to specify “default arguments”, which causes the compiler to implicitly fill them in if not provided.
do-something.cxx
1#include <string>
2
3void do_something(const std::string& a = "",
4 const std::string* b = NULL,
5 int n = 0) {
6 /* Lots of code here */
7}
Default arguments in C++ are fairly restrictive: No mandatory argument may follow an optional argument, and for a caller to specify one particular optional argument, all preceding arguments must be specified as well. However, these restrictions keep defaulted arguments intuitive and easy to use. Obviously, this means that Codehaus decided to remove all of them.
Defaulted arguments can be anywhere
Unlike C++, Groovy allows defaulted arguments to be anywhere within the argument list — beginning, middle, or end.
MiddleDefault.groovy
1package gl.lin;
2
3class MiddleDefault {
4 static doit(String left, String middle = "default", \
5 Object obj = null, String right) {
6 println("left=$left, middle=$middle, obj=$obj, right=$right");
7 }
8}
This relaxation in and of itself isn’t terrible — in a sane system (like OCaml, which specifically permits this), there is no ambiguity as long as all defaulted arguments are in one contiguous section.
Because of this, if we don’t pass any weird types into the function, we do in fact get fairly intuitive results.
1doit("foo", "bar")
2=> doit("foo", "default", null, "bar")
3
4doit("foo", "xyzzy", "bar")
5=> doit("foo", "xyzzy", null, "bar")
6
7doit("foo", null, "bar")
8=> doit("foo", null, null, "bar")
9
10doit("foo", "xyzzy", "baz", "bar")
11=> doit("foo", "xyzzy", "baz", "bar")
Defaulted arguments need not be contiguous
Since that made so much sense, Groovy went another step and made no requirement that optional arguments be in one contiguous group.
TripleDefault.groovy
1package gl.lin;
2
3class TripleDefault {
4 static doit(String a = "foo",
5 String b,
6 String c = "bar",
7 String d,
8 String e = "baz") {
9 println("a=$a, b=$b, c=$c, d=$d, e=$e");
10 }
11}
Any guesses as to how Groovy populates the defaulted arguments?
1doit("1", "2")
2=> doit("foo", "1", "bar", "2", "baz")
3
4doit("1", "2", "3")
5=> doit("1", "2", "bar", "3", "baz")
6
7doit("1", "2", "3", "4")
8=> doit("1", "2", "3", "4", "baz")
9
10doit("1", "2", "3", "4", "5")
11=> doit("1", "2", "3", "4", "5")
After looking at this for a bit, you might notice the method to Groovy’s madness. Given a function with N arguments, M of which are mandatory, and a call passing in K arguments, the first (K-M) optional arguments are “chosen”. Mandatory and chosen optional arguments are then populated from left to right, and the remaining optional arguments receive their default values.
Clearly, the insanity has not yet gone far enough.
Defaulted arguments interact with variadic arguments
Groovy inherits variadic functions from Java. In C++, if you create a function taking both optional arguments and variadic arguments, the variadic arguments can only be specified if all optional arguments are. This fits in with the strict left-to-right nature of argument population in the language. In contrast, Groovy takes a more mixed approach.
VariadicDefault.groovy
1package gl.lin;
2
3class VariadicDefault {
4 static doit(String a = "a", String b = "b", Object... rest) {
5 println("a=$a, b=$b, rest=${Arrays.asList(rest)}");
6 }
7}
As we saw last week, Groovy resolves function overloading based upon the dynamic types of the arguments it passed in. Given that defaulted arguments are internally implemented with overloading, you may be able to foresee the fun that ensues when defaults are mixed with variadic arguments.
1doit()
2=> doit("a", "b", {})
3
4doit("foo")
5=> doit("foo", "b", {})
6
7doit("foo", "bar")
8=> doit("foo", "bar", {})
9
10doit("foo", "bar", "baz")
11=> doit("foo, "bar", {"baz"})
12
13doit("foo", 1, "baz")
14=> doit("foo", "b", {1, "baz"})
15
16doit(1, "foo", "bar")
17=> doit("a", "b", {1, "foo", "bar"})
So it looks like if Groovy fails to assign a value to an optional argument, it puts the rest of the stuff into the variadic array. However, this example alone doesn’t give us enough information to determine the real rules.
Defaulted argument population has quadratic performance
For our last example, we’ll mix optional, mandatory, and variadic arguments.
Quadratic.groovy
1package gl.lin;
2
3class Quadratic {
4 static doit(String a = null, String b = null, String c = null,
5 String d = null, String m,
6 Object... rest) {
7 println("a=$a, b=$b, c=$c, d=$d, m=$m, rest=${Arrays.asList(rest)}");
8 }
9}
If you poke around at this function, you’ll find that Groovy tries really hard to make the arguments you give it fit.
1doit("foo")
2=> doit(null, null, null, null, "foo", {})
3
4doit("foo", "bar")
5=> doit("foo", null, null, null, "bar", {})
6
7doit("foo", "bar", "baz")
8=> doit("foo", "bar", null, null, "baz", {})
9
10doit("foo", 1)
11=> doit(null, null, null, null, "foo", {1})
12
13doit("foo", "bar", "baz", "quux", 3, "xyzzy", 2)
14=> doit("foo", "bar", "baz", null, "quux", {3, "xyzzy", 2})
It turns out that Groovy’s algorithm apparently is as follows. Given a function taking N arguments, of which M are mandatory, and K arguments passed in: Initially mark the first C=(K-M) optional arguments as chosen. Try to distribute given arguments left-to-right to the first C optional arguments and all mandatory arguments, adding the remainder to the variadic argument (if present). If that fails, decrement C and try again, until distribution succeeds or C becomes negative. This procedure has O(n2) run-time proportional to the number of arguments taken by the function.
Of course, things get even worse if you mix explicit overloading into this picture. We won’t be going that far this week, though.
In virtually every programming language, function calls are an extremely important operation, and their behaviour must be intuitive and implemented efficiently. Groovy fails at both: The manner in which default arguments are populated is surprisingly complex and hard to understand and incurs a large performance penalty. These problems are exacerbated by the issues involved in overloading, which is the basis for the internal implementation of default arguments and which would be the only alternative to Groovy’s built-in optional argument system.
Ultimately, this means that Groovy defaulted arguments are to be avoided as much as possible, as with overloading. A theoretically good feature of the language is rendered unusable due to careless design and poor implementation, built on top of another feature considered even more widely to be fundamentally broken.