Groovyのクロージャ ①クロージャ内のメソッド呼び出し基礎編

背景

JenkinsというCIツールではGroovyのコードでCI挙動を記述できるJenkins pipelineがあります。 Jenkins pipelineではGroovyのDSL(Domain-Specific Language:ドメイン固有言語)が使われています。 DSLにはクロージャが効果的に使用されています。

今回はクロージャの動作の基本編です。

クロージャとは

クロージャとは、生まれ故郷(クロージャ自身が定義された場所)を忘れない無名関数のことです。

こちらページが詳しいです。

koji-k.github.io

クロージャ内のメソッド呼び出し

生まれ故郷を忘れない無名関数とは何か、以下のプログラムで確認します。

MainクラスのclosureTest()内でPilpelineクラスのpipeline関数にクロージャ({}で囲んだ部分)を渡しています。 (クラス名と関数名はJenkins pipelineを意識しています)

クロージャの内部ではa()を呼び出しています。a()Mainクラス、Pipelineクラス両方で定義されています。

pipeline()メソッド内で、渡されたクロージャ(cl)を実行しています。

class Pipeline {
    def pipeline(Closure cl) {
      println "before closure"
      cl()
      println "after closure"
    }

    def a() {
        println "I'm Pipeline.a"
    }
}

class Main {
    def closureTest() {
        def pipeline = new Pipeline()
        pipeline.pipeline {
            a()
        }
    }
    
    def a() {
        println "I'm Main.a"
    }
}

def main = new Main()
main.closureTest()

このコードを実行します。出力は以下のようになりました。

before closure
I'm Main.a
after closure

クロージャが実行されたとき、Main.a()が呼ばれていることがわかります。

このように、クロージャは呼ばれた場所でなく、定義された(クロージャが書かれた)場所で実行されます。難しくいうと、クロージャは変数名やメソッドの名前解決を、(デフォルトでは)定義された場所で行う、ということです。クロージャを実行する側(今回はPipelineクラス)での名前解決は行われません。

呼び出し元のメソッドは呼ばれるか

確認として、Main.a()を削除してみましょう。それ以外は上記と同じコードです。

class Pipeline {
    def pipeline(Closure cl) {
      println "before closure"
      cl()
      println "after closure"
    }

    def a() {
        println "I'm Pipeline.a"
    }
}

class Main {
    def closureTest() {
        def pipeline = new Pipeline()
        pipeline.pipeline {
            a()
        }
    }
    
//    def a() {
//        println "I'm Main.a"
//    }
}

def main = new Main()
main.closureTest()

実行します。エラーになりました。

before closure
Exception thrown

groovy.lang.MissingMethodException: No signature of method: Main.a() is applicable for argument types: () values: []
Possible solutions: any(), tap(groovy.lang.Closure), any(groovy.lang.Closure), is(java.lang.Object), wait(), wait(long)
    at Main$_closureTest_closure1.doCall(closure_01.groovy:18)
    at Main$_closureTest_closure1.doCall(closure_01.groovy)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at Pipeline.pipeline(closure_01.groovy:4)
    at Pipeline$pipeline.call(Unknown Source)
    at Main.closureTest(closure_01.groovy:17)
    at Main$closureTest.call(Unknown Source)
    at closure_01.run(closure_01.groovy:36)
    at jdk.internal.reflect.GeneratedMethodAccessor60.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

これにより、クロージャを実行したクラス/メソッド(今回はPipeline.pipeline())に定義されたメソッドはクロージャ内からは呼ばれないことが分かりました。

環境

次回に続く

yucatio.hatenablog.com