How to make Groovy method signatures with Python-style kwargs and default values?

I may ask too much, but Groovy seems super flexible, so here goes ...

I would like the method in the class to be defined as follows:

class Foo { Boolean y = SomeOtherClass.DEFAULT_Y Boolean z = SomeOtherClass.DEFAULT_Z void bar(String x = SomeOtherClass.DEFAULT_X, Integer y = this.y, Boolean z = this.z) { // ... } } 

And to be able to specify only such arguments:

 def f = new Foo(y: 16) f.bar(z: true) // <-- This line throws groovy.lang.MissingMethodException! 

I am trying to provide an API that is flexible and type safe, which is the problem. This code is not flexible in that I would have to pass (and know as an API user) the default value for x in order to call the method. Here are some issues for the solution I want:

  • A security type is a mandatory signature - there is no void bar(Map) , unless the keys can be safe types. I understand that I could do type checking in the body of the method, but I try to avoid this level of redundancy, since I have a lot of this β€œkind” of method for writing.
  • I could use a class for each method signature - something like:

     class BarArgs { String x = SomeOtherClass.DEFAULT_X String y String z } 

    And define it as:

     void bar(BarArgs barArgs) { // ... } 

    And call it using my preferred method using the map constructor: f.bar(z: true) , but my problem is the default object on y . There is no way to handle this (what I know) without specifying it when calling the method, as in: f.bar(y: fy, z: true) . This is good for my small sample, but I look at 20-30 optional parameters for some methods.

Any suggestions (or questions if necessary) are welcome! Thanks for watching.

+5
source share
1 answer

Interest Ask. I interpreted your requirements like this

  • The class must have a default set of properties.
  • Each method must have a set of default arguments.
  • The default method overrides the default values ​​of the class.
  • Each method may have additional arguments that do not exist in the class.
  • The arguments to the method should not modify the class instance.
  • The arguments provided must be checked for the type.

I was not sure about number 5, as it is not explicitly listed, but it looks like this is what you wanted.

As far as I know, groovy has no built-in firmware to support all this, but there are several ways to make it work in an β€œeasy to use” way.

One of the ways that comes to mind is to create specialized argument classes, but use maps only as arguments in methods. With a simple superclass or tag to check and set properties, it is single-line to get the actual arguments for each method.

Here is a dash and some examples that can be used as a starting point:

 trait DefaultArgs { void setArgs(Map args, DefaultArgs defaultArgs) { if (defaultArgs) { setArgs(defaultArgs.toArgsMap()) } setArgs(args) } void setArgs(Map args) { MetaClass thisMetaClass = getMetaClass() args.each { name, value -> assert name instanceof String MetaProperty metaProperty = thisMetaClass.getMetaProperty(name) assert name && metaProperty != null if (value != null) { assert metaProperty.type.isAssignableFrom(value.class) } thisMetaClass.setProperty(this, name, value) } } Map toArgsMap() { def properties = getProperties() properties.remove('class') return properties } } 

With this attribute, it is easy to create specialized argument classes.

 @ToString(includePackage = false, includeNames = true) class FooArgs implements DefaultArgs { String a = 'a' Boolean b = true Integer i = 42 FooArgs(Map args = [:], DefaultArgs defaultArgs = null) { setArgs(args, defaultArgs) } } @ToString(includePackage = false, includeNames = true, includeSuper = true) class BarArgs extends FooArgs { Long l = 10 BarArgs(Map args = [:], FooArgs defaultArgs = null) { setArgs(args, defaultArgs) } } 

And a class that uses these arguments:

 class Foo { FooArgs defaultArgs Foo(Map args = [:]) { defaultArgs = new FooArgs(args) } void foo(Map args = [:]) { FooArgs fooArgs = new FooArgs(args, defaultArgs) println fooArgs } void bar(Map args = [:]) { BarArgs barArgs = new BarArgs(args, defaultArgs) println barArgs } } 

Finally, a simple test script; output method calls in comments

 def foo = new Foo() foo.foo() // FooArgs(a:a, b:true, i:42) foo.foo(a:'A') // FooArgs(a:A, b:true, i:42) foo.bar() // BarArgs(l:10, super:FooArgs(a:a, b:true, i:42)) foo.bar(i:1000, a:'H') // BarArgs(l:10, super:FooArgs(a:H, b:true, i:1000)) foo.bar(l:50L) // BarArgs(l:50, super:FooArgs(a:a, b:true, i:42)) def foo2 = new Foo(i:16) foo2.foo() // FooArgs(a:a, b:true, i:16) foo2.foo(a:'A') // FooArgs(a:A, b:true, i:16) foo2.bar() // BarArgs(l:10, super:FooArgs(a:a, b:true, i:16)) foo2.bar(i:1000, a:'H') // BarArgs(l:10, super:FooArgs(a:H, b:true, i:1000)) foo2.bar(l:50L) // BarArgs(l:50, super:FooArgs(a:a, b:true, i:16)) def verifyError(Class thrownClass, Closure closure) { try { closure() assert "Expected thrown: $thrownClass" && false } catch (Throwable e) { assert e.class == thrownClass } } // Test exceptions on wrong type verifyError(PowerAssertionError) { foo.foo(a:5) } verifyError(PowerAssertionError) { foo.foo(b:'true') } verifyError(PowerAssertionError) { foo.bar(i:10L) } // long instead of integer verifyError(PowerAssertionError) { foo.bar(l:10) } // integer instead of long // Test exceptions on missing properties verifyError(PowerAssertionError) { foo.foo(nonExisting: 'hello') } verifyError(PowerAssertionError) { foo.bar(nonExisting: 'hello') } verifyError(PowerAssertionError) { foo.foo(l: 50L) } // 'l' does not exist on foo 
+6
source

All Articles