Reputation: 11042
I have method
def test(String a, String b) { }
and I would like to call this with a dynamic parameter map. I always though that
test(['1','2']); //valid call
and also
test([a:'1',b:'2']); //=> does not work
will work. but it doesn't. So I remembered the spread operator, but can't get it to work....
Is there a way to call a method like the one above with some kind of map as parameter instead of single parameters?
Upvotes: 72
Views: 125580
Reputation: 11042
Thanks to the comment of Will P, I've found a solution which fits my problem:
If I define one parameter without a type, I can pass in all kinds of types, including hashMaps. And groovy turns a construct like a:'h',b:'i'
automagically into a hashmap
def test(myParams, Integer i) {
return myParams.a + myParams.b
}
assert test(a:'h',b:'i',5) == test(b:'i',a:'h',5)
assert test([a:'h',b:'i'],5) == test(b:'i',a:'h',5)
test('h','i',5); //still throws an exception
This way, I can use single named parameters, but can use a Map too!
Upvotes: 8
Reputation: 26839
The method call should be test(a: '1', b: '2')
instead of test([a: '1', b: '2'])
.
Please check the named parameters section for methods in the docs.
Upvotes: 49
Reputation: 35853
The named parameters support is pretty flexible, but the docs are a bit thin. Here are some of the rules I've discovered. Note that I'm trying to be unambiguous in the use of the terms "parameters" (declared in the method) and "args" (passed to the method call)
(a: "aa")
is good enough, you don't need ([a: "aa"])
x
in the examples below)args=[:]
making
the named args optional, but this doesn't work well if you have
other optional parameters (see last examples below)Here are some examples: The params don't need to be typed, but I've added types for clarity.
// this method has a map args to capture all named args
// and non-named (ordered) args String s, int n, and int x
// x has a default value so is optional
// the map (here untyped) to capture the nameed args MUST COME FIRST
def m(Map args=[:], String s, int n, int x=1)
{
println "s:$s n:$n x:$x, args:$args"
}
//1: pass in named args first, then ordered
m(a: "aa", b: 3, "ss", 44, 5) // s:ss n:44 x:5, args:[a:aa, b:3]
//2: ordered args first - named args last (same result)
m("ss", 44, 5, a: "aa", b: 3) // s:ss n:44 x:5, args:[a:aa, b:3]
//3: bring the first ordered arg (s) to the start (same result)
m("ss", a: "aa", b: 3, 44, 5) // s:ss n:44 x:5, args:[a:aa, b:3]
//4: stick the ordered arg n in the middle of the named args (same result!)
m("ss", a: "aa", 44, b: 3, 5) // s:ss n:44 x:5, args:[a:aa, b:3]
//5: mix the ordered args in with the named and SKIP the arg x with default value (x=1)
m(a: "aa", "ss", b: 3, 44) // s:ss n:44 x:1, args:[a:aa, b:3]
//6: ordered arg n first - so in the wrong order (Fail!)
//m(44, "ss", a: "aa", b: 3, 5) //MissingMethodException: No signature .. of .. m() .. applicable for
// argument types: (java.util.LinkedHashMap, java.lang.Integer, java.lang.String, java.lang.Integer)
// values: [[a:aa, b:3], 44, ss, 5]
//7: no named args: Fails! (change signature to add default: Map args=[:] and it will succeed with: s:ss n:44 x:1, args:[:]
m("ss", 44) // ...No signature ... applicaple ... types (java.lang.String, java.lang.Integer)
//8: no named args: Fails! (even with default map in signature this fails!)
m("ss", 44, 5) // ...No signature ... applicaple ... types (java.lang.String, java.lang.Integer, java.lang.Integer)
Upvotes: 31
Reputation: 62789
This question got me thinking and I came up with a flexible, interesting (if not scary) solution.
This signature seems to take absolutely any combination of arguments:
f(Map m=null, Object... obj)
and it does mostly predictable things with them.
The only irritating strangeness is the case where you pass a map as the first positional parameter.
f([a:1], 2) has a map m[a:1] and one obj[0]=2
however
f([a:1], b:2)
has a map m of [b:2] and a obj[0]= a map [a:1] so you wouldn't be able to tell for sure if the map m was a positional map or a named parameter.
By the way, I don't necessarily recommend any of this, I'd use exact parameters whenever possible (and I even prefer explicit types). There are quite a few times in my own code where I've gotten a little confused and fixed things up by adding some explicit typing, however in some cases this could be quite useful.
Upvotes: 1
Reputation: 6320
I absolutely hate how groovy does positional and named/default arguments. It's terrible. Python does it right without question.
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.
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.
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?
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.
Upvotes: 10
Reputation: 14539
Maybe I missed something, but I don't think Groovy has named parameters right now. There are discussions and proposals, but I'm not aware of anything official.
For your case, I think the map spread may help, but not in every case. Upon getting the values, it follows the order in which the map values were declared:
def test(String a, String b) { "a=$a, b=$b" }
def test(Map m) { test m*.value }
assert test(a: "aa", b:"bb") == "a=aa, b=bb"
assert test(b: "aa", a:"bb") != "a=aa, b=bb" // should be false :-(
assert test(b: "ccc", a:"ddd") == "a=ddd, b=ccc" // should have worked :-(
For classes, may I suggest Groovy's as operator?
@groovy.transform.CompileStatic
class Spread {
class Person {
String name
BigDecimal height
}
def method(Person p) {
"Name: ${p.name}, height: ${p.height}"
}
def method(Map m) { method m as Person }
static main(String[] args) {
assert new Spread().method(name: "John", height: 1.80) ==
"Name: John, height: 1.80"
}
}
Upvotes: 35