Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Groovy 2.4 variable scope in closure with @Field annotation

Tags:

groovy

Can someone explain to me why in closure2 initVars('c') is not able to modify the referenced object if @Field is used in declaration?

import groovy.transform.Field;

@Field def lines4 = "a";

void initVars(String pref){
    println('init:'+lines4+'  '+pref)      //*3.init:a  b   *7.init:b  c
    lines4 = pref;  
}
println("closure1")    ///1. closure1
1.times {
    println(lines4)    ///2. a
    initVars('b')      ///3. init:a  b
    lines4 += 'p1'
    println(lines4)    ///4. bp1
}
println("closure2")    ///5. closure2
1.times {
    println(lines4)    ///6. bp1
    initVars('c')      ///7. init:b  c
    println(lines4)    ///8. bp1     Why not c
    lines4 += 'q1'
    println(lines4)    ///9. bp1q1   Why not cq1
}

Output:

C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a  b
4. bp1
5. closure2
6. bp1
7. init:b  c
8. bp1
9. bp1q1

Output without @Field and def, with just lines4 = "a" in script scope. This appears normal to me.

C:\projects\ATT>groovy test.groovy
1. closure1
2. a
3. init:a
4. bp1
5. closure2
6. bp1
7. init:bp1
8. c
9. cq1

I saw same behavior in groovy2.5-beta and groovy 2.6-alpha.

like image 897
samarjit samanta Avatar asked Dec 13 '22 20:12

samarjit samanta


1 Answers

Using @Field annotation on a script variable changes a scope of this variable from a local one to a Script class one:

Variable annotation used for changing the scope of a variable within a script from being within the run method of the script to being at the class level for the script.

The annotated variable will become a private field of the script class. The type of the field will be the same as the type of the variable. Example usage:

import groovy.transform.Field
@Field List awe = [1, 2, 3]
def awesum() { awe.sum() }
assert awesum() == 6

In this example, without the annotation, variable awe would be a local script variable (technically speaking it will be a local variable within the run method of the script class). Such a local variable would not be visible inside the awesum method. With the annotation, awe becomes a private List field in the script class and is visible within the awesum method.

Source: http://docs.groovy-lang.org/2.4.12/html/gapi/groovy/transform/Field.html

Every Groovy script extends groovy.lang.Script class and the body of the script is executed inside Script.run() method. Groovy passes variables to this script using Binding object. When you change a scope of a local script variable to a class level then there is no binding for this variable passed to a closure, because binding object contains only local-scoped variables. Compare these two screenshots I made. First one shows what the binding object looks like when we call initVars(String pref) for the first time and lines4 is a local script variable:

enter image description here

And here is same breakpoint but now lines4 is a @Field def lines4 variable:

enter image description here

As you can see there is no binding for lines4 variable in binding object, but there is a class field called lines4, while this binding is present in the first screenshot attached.

When you call

lines4 += 'p1'

in the first closure, local binding for lines4 is created and it is initialized with a current value of a this.lines4 value. It happens because Script.getProperty(String property) is implemented in following way:

public Object getProperty(String property) {
    try {
        return binding.getVariable(property);
    } catch (MissingPropertyException e) {
        return super.getProperty(property);
    }
}

Source: https://github.com/apache/groovy/blob/GROOVY_2_4_X/src/main/groovy/lang/Script.java#L54

So it firstly checks if there is a binding for a variable you access in the closure and when it does not exist it passes execution to a parent's getProperty(name) implementation - in our case it just returns class property value. At this point this.lines4 is equal to b and this is the value that is returned.

initVars(String pref) method accesses class field, so when you call it it always overrides Script.lines4 property. But when you call

lines4 += 'q1'

in the second closure, the binding lines4 for a closure already exists and its value is bp1 - this value was associated in the first closure call. That's why you don't see c after calling initVars('c'). I hope it helps.

UPDATE: How binding works in a script explained

Let's get a little deeper to get better understanding what is going on under the hood. This is what your Groovy script looks like when it is compiled to a bytecode:

Compiled from "script_with_closures.groovy"
public class script_with_closures extends groovy.lang.Script {
  java.lang.Object lines4;
  public static transient boolean __$stMC;
  public script_with_closures();
  public script_with_closures(groovy.lang.Binding);
  public static void main(java.lang.String...);
  public java.lang.Object run();
  public void initVars(java.lang.String);
  protected groovy.lang.MetaClass $getStaticMetaClass();
}

Two things worth mentioning at this moment:

  1. @Field def lines4 is compiled to a class field java.lang.Object lines4;
  2. void initVars(String pref) method is compiled to public void initVars(java.lang.String); class method.

For a simplicity you can assume that the rest content (excluding lines4 and initVars method) of your script is inlined to public java.lang.Objectrun() method.

initVars always accesses class field lines4 because it has direct access to this field. Decompiling this method to a bytecode shows us this:

  public void initVars(java.lang.String);
    Code:
       0: invokestatic  #19                 // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
       3: astore_2
       4: aload_2
       5: ldc           #77                 // int 5
       7: aaload
       8: aload_0
       9: aload_2
      10: ldc           #78                 // int 6
      12: aaload
      13: aload_2
      14: ldc           #79                 // int 7
      16: aaload
      17: aload_2
      18: ldc           #80                 // int 8
      20: aaload
      21: ldc           #82                 // String init:
      23: aload_0
      24: getfield      #23                 // Field lines4:Ljava/lang/Object;
      27: invokeinterface #67,  3           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      32: ldc           #84                 // String
      34: invokeinterface #67,  3           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      39: aload_1
      40: invokeinterface #67,  3           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      45: invokeinterface #52,  3           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.callCurrent:(Lgroovy/lang/GroovyObject;Ljava/lang/Object;)Ljava/lang/Object;
      50: pop
      51: aload_1
      52: astore_3
      53: aload_3
      54: aload_0
      55: swap
      56: putfield      #23                 // Field lines4:Ljava/lang/Object;
      59: aload_3
      60: pop
      61: return

Operation 56 is a opcode for a assigning value to a field.

Now let's understand what happens when both closures gets called. First thing worth mentioning - both closures have delegate field set to the script object that is being executed. We know that it extends groovy.lang.Script class - a class that uses binding private field to store all bindings (variables) available in the script runtime. This is important observation, because groovy.lang.Script class overrides:

  • public Object getProperty(String property)
  • public void setProperty(String property, Object newValue)

Both methods use binding to lookup and store variables used in the script runtime. getProperty gets called any time you read local script variable and setProperty gets called any time you assign a value to script local variable. That's why code like:

lines4 += 'p1'

generates sequence like:

getProperty -> value + 'p1' -> setProperty

In your example first attempt of reading lines4 ends up with returning a value from parent class (it happens if binding is not found, then GroovyObjectSupport.getProperty(name) is called and this one returns a value of a class property with given name). When closure assigns a value to a lines4 variable then a binding is created. And because both closures share same binding object (they use delegate to the same instance), when second closure reads or writes line4 variable then it uses previously created binding. And initVars does not modify binding because as I shown you earlier it accesses class field directly.

like image 68
Szymon Stepniak Avatar answered May 23 '23 09:05

Szymon Stepniak