なぜGradleは理解しづらいのか?

2018年7月13日

Gradleは非常にわかりづらい。このわかりづらさは、一つには巷にあふれる初心者向けの説明にその責任がある。曰く、

  • GradleはGroovy言語である。
  • メソッド名を最初に書き、続けて引数を記述する。
  • 中括弧はクロージャと言って、これも実行対象となる。

これらは間違いでも無いのだが、しかし正しくも無い。その理由を以下に記述する。ただし、私はGroovyをまともに使ったことはない。あちこちでの聞きかじりの知識を元に説明しようとこころみるものであるので、間違いはあるかもしれない。

そもそもGroovy言語には、その構文構造から逸脱する仕組みが提供されている

あらゆる言語には、その字面上の構文というものがあり、それを逸脱するものは何らかのエラーになるはずなのだが、Groovyにはこれが当てはまらない。例えば、「何らかの名前+引数」という字面が記述されてあっても、必ずしもそれはメソッド呼び出しを意味はしない。この仕組みとしては、AST変換とmissingMethod/invokeMethodという二つの仕組みがあるらしいのだが、簡単のためにmissingMethodの例を説明してみる。

以下の例をbuild.gradleとして記述し、タスク名無しでgradleコマンドを実行してみる。あるいは、「gradle -q」等でもよい。

/** これはJavaのクラスと同じような定義で、理解は容易だろう */
class Sample {
  String toText
  List others = new ArrayList();

  /** toというメソッドの定義 */
  def to(String toText) {
    this.toText = toText
  }

  /** これはmethodMissingというメソッドのオーバライドになる */
  def methodMissing(String methodName, args) {
    others.add(methodName);
  }

  /** 全部printしてみるメソッド */  
  def printAll() {
    println "to:" + toText
    others.each{println it}
  }
}

// clというクロージャを定義する 
def cl = {
  println "started"
  to "foo"
  from "bar"
  "foobar" "barfoo"
  printAll()
}

// このクロージャのdelegateにSampleのインスタンスを格納し、クロージャを実行する
cl.delegate = new Sample();
cl();

この実行結果は以下になる。

started
to:foo
from
foobar

クロージャの定義と呼び出し

コードのお尻の方から説明する。まずclという変数にクロージャを代入している。クロージャというのはJavaで言うところのラムダ式のようなものだから、例えて言えば、

Runnable cl = ()-> {
 ....
}

といったところになる。そのクロージャのdelegate(後述)にSampleのインスタンスを代入し、クロージャを呼び出している。Javaで書くと、

cl.run()

といったところ。

delegateとクロージャ内での呼び出し

Groovyのクロージャにはdelegateという変数が用意されており、そこに代入されたオブジェクトのメソッドは、オブジェクトを指定しなくとも呼び出しが可能というルールがある。だから、ここにSampleのインスタンスを入れると、インスタンスを指定せずにSampleのメソッドを呼び出せることになる。

クロージャの中身を見てみれば、

def cl = {
  ....
  to "foo"
  ....
  printAll()
}

として、Sample#to、Sample#printAllを呼び出していることがわかる。

存在しないメソッドの呼び出しはどうなるか?

クロージャ内で呼び出されているその他のメソッドとして、printlnとfromがある。printlnは元々どこからでも呼び出させるようになっているので問題は無いのだが、fromの方はどこにも定義が無い。

実は、メソッド定義の無い場合には、methodMissingが呼び出されるのである。しかもこれは「メソッド名にあたる部分」が識別子でない場合にも適用される。だから、

def cl = {
  ...
  from "bar"
  "foobar" "barfoo"
  ....
}

の二行分については、methodMissingが呼び出され、Sampleインスタンス内部のothersというリストに保持されることになる。

methodMissingとDSL

実際にGradle内部でmethodMissingのみを使っているのか、あるいはinvokeMethodやAST変換を使っているのかは定かではないのだが、ともあれ、methodMissingを使っても、構文構造から逸脱するような記述ができることがわかる。

例えば、対応するメソッドを一切用意していなくても、次のような記述をした場合に、それを適当に処理することができるのだ。構文としては、「メソッド呼び出しであるにも関わらず」である。

def cl = {
  to "foo"
  from "bar"
  subject "test required"
  attached1 "file1"
  attached2 "file2"
  content "test is mandatory. do it immediately"
}

そして、これらの処理のされかたというのは、完全にdelegateに与えられるクラスに依存している。そのクラス内での処理によって、どのような記述が可能かが決まるのである。

逆に言えば、ここで記述可能なものとしては、そのクラスの扱う問題領域の便利な記述方法として最適なものになっているわけだ。これをDSL(Domain Specific Language)などと呼ぶのであるが、なんのことはない、クロージャ内に記述されたものをどう解釈するかという話である。

DSLを説明する確固とした枠組みが無いこと

しかし、Gradleでは、これがそれぞれのクラスでまちまちである。つまり、クラスごとに基本的にはDSLが異なっており、それを統一的に説明する仕組みが存在しないようだ。だから、Gradleのマニュアルでは、「例えばこういう場合はこう」「こうもできる」などという説明しかされていない(おおよそは)。

つまり、Gradleでは、

  • 最初のワードがメソッド名とは限らない。場合によりけり。
  • クロージャに記述可能な「構造」は、何をdelegateしているかによって全く異なる。

という点がJavaのようなプログラミング言語とは異なっているのである。これが初学者が理解しづらい最大の原因と思われる。