Groovy
  1. Groovy
  2. GROOVY-6134

Make @DelegatesTo support mapping to generic type argument

    Details

    • Type: Improvement Improvement
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 2.1.3
    • Fix Version/s: 2.2.0-beta-1
    • Component/s: None
    • Labels:
      None

      Description

      Following the discussion at https://gist.github.com/alkemist/5429603, it would be interesting if Groovy allowed @DelegatesTo to map onto a generic type argument.

      For example:

      public <T> Object map(@DelegatesTo.Target List<T> target,  @DelegatesTo(genericTypeIndex=0) Closure arg) {
          arg.delegate = target.join('')
          arg()
      }
      def test() {
          def result
          map(['f','o','o']) {
              result = toUpperCase()
          }
      
          result
      }
      assert 'FOO'==test()
      

        Activity

        Show
        Cédric Champeau added a comment - See https://github.com/melix/groovy-core/commit/ddac9b0d0487d92042714bec7700e119e47381ae for implementation.
        Hide
        Peter Niederwieser added a comment -

        Can you also make this work for Class<T> target set to a class literal? Would be very useful.

        Show
        Peter Niederwieser added a comment - Can you also make this work for Class<T> target set to a class literal? Would be very useful.
        Hide
        Peter Niederwieser added a comment -

        It seems that @TypeChecked and @CompileStatic cannot currently infer the generic type argument of a class literal in general. E.g. for a method that takes Class<T> and returns T.

        Show
        Peter Niederwieser added a comment - It seems that @TypeChecked and @CompileStatic cannot currently infer the generic type argument of a class literal in general. E.g. for a method that takes Class<T> and returns T .
        Hide
        Cédric Champeau added a comment -

        Do you mean you want to write @DelegatesTo(T) ? If so, this is not possible (and why we chose the genericTypeIndex solution) because the JVM doesn't support use of generics placeholders as parameter values of annotations. Groovy doesn't complain if you do so, but information is lost, so the type checker is unable to use it.

        Show
        Cédric Champeau added a comment - Do you mean you want to write @DelegatesTo(T) ? If so, this is not possible (and why we chose the genericTypeIndex solution) because the JVM doesn't support use of generics placeholders as parameter values of annotations. Groovy doesn't complain if you do so, but information is lost, so the type checker is unable to use it.
        Hide
        Peter Niederwieser added a comment -

        No, that's not what I want. I want a way to implement the (type checked and/or statically compiled) configure method below:

        class Car {
            String brand
        }
        
        configure(Car) {
            brand = "BMW"    
        }
        
        Show
        Peter Niederwieser added a comment - No, that's not what I want. I want a way to implement the (type checked and/or statically compiled) configure method below: class Car { String brand } configure(Car) { brand = "BMW" }
        Hide
        Cédric Champeau added a comment -

        Normally you would just write:

        public void configure(@DelegatesTo.Target Object target, @DelegatesTo Closure cl) { ... }
        
        configure(Car) {
           brand = 'BMW'
        }
        
        
        Show
        Cédric Champeau added a comment - Normally you would just write: public void configure(@DelegatesTo.Target Object target, @DelegatesTo Closure cl) { ... } configure(Car) { brand = 'BMW' }
        Hide
        Peter Niederwieser added a comment -

        Cool, this (surprisingly) works. However, I want to express that target needs to be of type Class.

        Show
        Peter Niederwieser added a comment - Cool, this (surprisingly) works. However, I want to express that target needs to be of type Class .
        Hide
        Peter Niederwieser added a comment -

        More precisely, target needs to be of type Class<T>, and the method returns T.

        Show
        Peter Niederwieser added a comment - More precisely, target needs to be of type Class<T> , and the method returns T .
        Hide
        Cédric Champeau added a comment -

        This works:

        class Car { String brand }
        public <T> T configure(@DelegatesTo.Target Class<T> target, @DelegatesTo(genericTypeIndex=0) Closure cl) {  }
        
        
        @groovy.transform.TypeChecked
        void foo() {
           Car c = configure(Car) {
              brand = 'BMW'
           }
        }
        

        Enjoy!

        Show
        Cédric Champeau added a comment - This works: class Car { String brand } public <T> T configure(@DelegatesTo.Target Class <T> target, @DelegatesTo(genericTypeIndex=0) Closure cl) { } @groovy.transform.TypeChecked void foo() { Car c = configure(Car) { brand = 'BMW' } } Enjoy!
        Hide
        Peter Niederwieser added a comment -

        My expectation is that def <T> T configure(@DelegatesTo.Target Class<T> target, @DelegatesTo(genericTypeIndex=0) Closure closure) does the trick. But currently, neither the delegate type nor the return type of {{configure(Car)

        { brand = "BMW" }

        }} is inferred correctly.

        Show
        Peter Niederwieser added a comment - My expectation is that def <T> T configure(@DelegatesTo.Target Class<T> target, @DelegatesTo(genericTypeIndex=0) Closure closure) does the trick. But currently, neither the delegate type nor the return type of {{configure(Car) { brand = "BMW" } }} is inferred correctly.
        Hide
        Peter Niederwieser added a comment -

        Interesting. I could swear this didn't work for me. Will have to investigate.

        Show
        Peter Niederwieser added a comment - Interesting. I could swear this didn't work for me. Will have to investigate.
        Hide
        Cédric Champeau added a comment -

        This is exactly what I've written and it passes, so I suspect it only works on master and 2.1.7-SNAPSHOT (lots of generics fixes in there).

        P.S: Tested on 2.1.6 and it fails, so my guess seems to be right, 2.1.7 will infer correctly.

        Show
        Cédric Champeau added a comment - This is exactly what I've written and it passes, so I suspect it only works on master and 2.1.7-SNAPSHOT (lots of generics fixes in there). P.S: Tested on 2.1.6 and it fails, so my guess seems to be right, 2.1.7 will infer correctly.
        Hide
        Peter Niederwieser added a comment -

        After double-checking, above example does work in GroovyConsole 2.2.0-beta-1, which is great. However, it apparently stops working once configure and foo are declared in different classes. For example, the following isn't working:

        @groovy.transform.CompileStatic
        class Car {
          String brand
        }
        
        @groovy.transform.CompileStatic
        class Builder {
         def <T> T configure(@DelegatesTo.Target Class<T> target, @DelegatesTo(genericTypeIndex=0) Closure cl) {
          def obj = target.newInstance() 
          cl.delegate = obj
          cl.resolveStrategy = Closure.DELEGATE_FIRST
          cl.call()
          obj 
         }
        }
        
        @groovy.transform.CompileStatic
        class Main {
         void run() {
          def builder = new Builder()
          def car = builder.configure(Car) {
            setBrand('BMW') // have to use method syntax (known issue)
          }
          assert car.brand == "BMW" // [Static type checking] - No such property: brand for class: java.lang.Object
         }
        }
        
        new Main().run()
        
        Show
        Peter Niederwieser added a comment - After double-checking, above example does work in GroovyConsole 2.2.0-beta-1, which is great. However, it apparently stops working once configure and foo are declared in different classes. For example, the following isn't working: @groovy.transform.CompileStatic class Car { String brand } @groovy.transform.CompileStatic class Builder { def <T> T configure(@DelegatesTo.Target Class <T> target, @DelegatesTo(genericTypeIndex=0) Closure cl) { def obj = target.newInstance() cl.delegate = obj cl.resolveStrategy = Closure.DELEGATE_FIRST cl.call() obj } } @groovy.transform.CompileStatic class Main { void run() { def builder = new Builder() def car = builder.configure(Car) { setBrand('BMW') // have to use method syntax (known issue) } assert car.brand == "BMW" // [Static type checking] - No such property: brand for class: java.lang. Object } } new Main().run()
        Hide
        Peter Niederwieser added a comment -

        Actually, property syntax does work here, and the delegate type can be inferred as well. Only the return type can't be inferred. Inserting a cast (assert ((Car) car).brand == "BMW") solves the problem.

        Show
        Peter Niederwieser added a comment - Actually, property syntax does work here, and the delegate type can be inferred as well. Only the return type can't be inferred. Inserting a cast ( assert ((Car) car).brand == "BMW" ) solves the problem.

          People

          • Assignee:
            Cédric Champeau
            Reporter:
            Cédric Champeau
          • Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development