I absolutely hate how groovy does positional and named/default arguments. It's terrible. Python does it right without question.
Problems
- Calling a function with argument names actually creates a map and makes that map the first argument.
code
test(a:"a", b: "b") // Actual myfunc([a: "a", b: "b"])
test("a", b: "b") // Actual myfunc([b: "b"], "a")
test(a: "a", "b") // Actual myfunc([a: "a"], "b")
This is bad because it actually changes the order of the positional arguments.
- Normal default arguments cannot be called out of order
code
def test(String a, String b, int x=1, int y=2){
a = args.get('a', a)
b = args.get('b', b)
x = args.get('x', x)
y = args.get('y', y)
println "a:$a b:$b x:$x, y:$y"
}
test("a", 'b') // Positional arguments without giving the default values
// "a:a b:b x:1 y:2"
test("a", "b", 3) // Positional arguments with giving 1 default and not the last
// "a:a b:b x:3 y:2"
test("a", "b", y:4) // Positional with Keyword arguments. Actual call test([y:4], "a", "b")
// This fails!? No signature of method, because Map is the first argument
Of course you can always override the function to make the arguments match the position you want. This is just a huge hassle when you have lots of arguments.
- Using a Map as the first argument does not allow pure positional arguments
code
def test1(Map args=[:], String a, String b, int x=1, int y=2){
a = args.get('a', a)
b = args.get('b', b)
x = args.get('x', x)
y = args.get('y', y)
println "test1(a:$a b:$b x:$x, y:$y, args:$args)"
}
test1("ss", "44", 5, c: "c", d: 3) // Actual test2([c: "c", d: 3], "ss", "44", 5) Matches our definition
// test1(a:ss b:44 x:5, y:2, args:[c:c, d:3, a:ss, b:44, x:5, y:2])
test1(a: "aa", b: 3, "ss", "44", 5) // Actual test2([a: "aa", b: 3], "ss", "44", 5) Nothing wrong with repeat parameters because they are in the map
// test1(a:aa b:3 x:5, y:2, args:[a:aa, b:3, x:5, y:2])
test1(a: "aa", b: 3, "ss", "44", y:5) // Actual test2([a: "aa", b: 3, y:5], "ss", "44") y is in the map, so y still has the default positional value
// test1(a:aa b:3 x:1, y:5, args:[a:aa, b:3, y:5, x:1])
test1("ss", "44", y:3) // Actual test2([y:3], "ss", "44")
// test1(a:ss b:44 x:1, y:3, args:[y:3, a:ss, b:44, x:1])
test1('a', 'b') // Pure positional arguments only required arguments given (no defaults given)
// test1(a:a b:b x:1, y:2, args:[a:a, b:b, x:1, y:2])
test1("ss", "44", 5) // Pure positional arguments one missing
// This fails!? No signature of method. Why?
test1("ss", "44", 5, 6) // Pure positional arguments all arguments given
// This fails!? No signature of method. Why?
My Solution ...
Ultimately my solution was to take in any number of arguments as Objects and to map those arguments with a defined Map of arguments.
code
// Return a Map of arguments with default values. Error if argument is null
def mapArgs(Object args, Map m){
Map check = [:]
def offset = 0
// Check if first argument is map and set values
if (args[0] instanceof Map){
check = args[0]
offset += 1
check.each{ subitem ->
m[subitem.key] = subitem.value
}
}
// Iter positional arguments. Do not replace mapped values as they are primary.
m.eachWithIndex{ item, i ->
m[item.key] = ((i + offset) < args.size() && !check.containsKey(item.key)) ? args[i + offset] : item.value
if (m[item.key] == null){
throw new IllegalArgumentException("Required positional argument ${item.key}")
}
}
return m
}
def test2(Object... args) {
// println "args $args"
def m = mapArgs(args, [a: null, b: null, x: 1, y:2])
println "test2(a:$m.a b:$m.b x:$m.x, y:$m.y, args:null)"
}
test2("ss", "44", 5, c: "c", d: 3) // Actual test2([c: "c", d: 3], "ss", "44", 5) Matches our definition
// test1(a:ss b:44 x:5, y:2, args:[c:c, d:3, a:ss, b:44, x:5, y:2])
// test2(a:ss b:44 x:5, y:2, args:null)
test2(a: "aa", b: 3, "ss", "44", 5) // Actual test2([a: "aa", b: 3], "ss", "44", 5) Nothing wrong with repeat parameters because they are in the map
// test1(a:aa b:3 x:5, y:2, args:[a:aa, b:3, x:5, y:2])
// test2(a:aa b:3 x:5, y:2, args:null)
test2(a: "aa", b: 3, "ss", "44", y:5) // Actual test2([a: "aa", b: 3, y:5], "ss", "44") y is in the map, so y still has the default positional value
// test1(a:aa b:3 x:1, y:5, args:[a:aa, b:3, y:5, x:1])
// test2(a:aa b:3 x:1, y:5, args:null)
test2("ss", "44", y:3) // Actual test2([y:3], "ss", "44")
// test1(a:ss b:44 x:1, y:3, args:[y:3, a:ss, b:44, x:1])
// test2(a:ss b:44 x:1, y:3, args:null)
test2('a', 'b') // Pure positional arguments only required arguments given (no defaults given)
// test1(a:a b:b x:1, y:2, args:[a:a, b:b, x:1, y:2])
// test2(a:a b:b x:1, y:2, args:null)
test2("ss", "44", 5) // Pure positional arguments one missing
// This fails!? No signature of method. Why?
// test2(a:ss b:44 x:5, y:2, args:null)
test2("ss", "44", 5, 6) // Pure positional arguments all arguments given
// This fails!? No signature of method. Why?
// test2(a:ss b:44 x:5, y:6, args:null)
I'm not really happy with this solution, but it makes keyword arguments work for my needs.