Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do Groovy method signatures with Python-style kwargs AND default values?

Tags:

groovy

I might be asking too much, but Groovy seems super flexible, so here goes...

I would like a method in a class to be defined like so:

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 provide only certain arguments like so:

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 both flexible and type safe, which is the problem. The given code is not flexible in that I would have to pass in (and know as the user of the API) the default value for x in order to call the method. Here are some challenges for the solution I want:

  • Type safety is a must--no void bar(Map) signatures unless the keys can somehow be made type safe. I realize with this I could do the type checking in the method body, but I'm trying to avoid that level of redundancy as I have many of this "kind" of method to write.
  • 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 like:

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

    And call it using my desired way using the map constructor: f.bar(z: true), but my problem lies in the object's default on y. There's no way to handle that (that I know of) without having to specify it when calling the method as in: f.bar(y: f.y, z: true). This is fine for my little sample, but I'm looking at 20-30 optional parameters on some methods.

Any suggestions (or questions if needed) are welcome! Thank you for taking a look.

like image 472
Andy Avatar asked Jun 09 '16 20:06

Andy


1 Answers

Interesting question. I've interpreted your requirements like this

  1. The class should have a set of default properties.
  2. Each method should have a set of default arguments.
  3. The method defaults override the class defaults.
  4. Each method can have additional arguments, not existing on the class.
  5. The method arguments should not modify the class instance.
  6. Provided arguments needs to be checked for type.

I was not sure about number 5 since it is not explicitly specified, but it looked like that was what you wanted.

As far as I know, there is nothing built-in in groovy to support all this, but there are several ways to make it work in a "simple-to-use" manner.

One way that comes to mind is to create specialized argument classes, but only use maps as the arguments in the methods. With a simple super-class or trait to verify and set the properties, it is a one-liner to get the actual arguments for each method.

Here is a trait 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 trait is it 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 of method invocations 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
like image 98
Steinar Avatar answered Nov 15 '22 12:11

Steinar