G*Magazine Vol.6

Groovy臨機応変(第一回) 〜動中の静…Groovy 2.1.0の新機能その1〜

はじめに

こんにちは、JGGUGの上原(NTTソフトウェア株式会社所属)です。G*マガジンに記事を書くのは初めてとなります。このシリーズでは、Groovyの機能について紹介していきます。今回および次回では、この記事が掲載されているころには既にリリース済みの予定である、Groovy 2.1.0で導入された一連の新機能について紹介していく予定です。なお、今回の記事の内容は記事執筆時点で入手可能なGroovy 2.1.0 RC1バージョンに基づいており、最終リリース版では仕様や動作が変更されている可能性もあることをあらかじめご了承ください。

Groovy 2.1.0は、メジャーバージョンアップであった2.0に引き続くマイナーバージョンアップです。Groovy 2.0リリースはインフラレベルの大きな機能拡張をドカンと提供するものでしたが、それを踏まえての2.1.0リリースは、2.0で導入された機能をより使いやすく発展させるための機能が含まれており、実用性を高めるための周辺手段が整って来たような印象です。Groovy 2.0の新機能については参考[1]、[2]などを参考下さい。

今回は、Groovy 2.1.0の新機能の中で「静的Groovy」機能に関連のある以下について紹介します。

  • @DelegatesTo
  • 型チェックを拡張し、カスタマイズする
  • コンパイラ設定スクリプト
  • コンパイラ設定ビルダ

Groovy 2.0及び2.1の新機能については、参考[1](プログラミングGROOVY別冊:第8章 Groovy 2.0の新機能)、[4]なども参考にしてください。

@DelegatesTo

@DelegatesToは静的型チェックを強化するために導入された新規のAST変換アノテーションであり、groovy.transform.*パッケージに所属します。

@DelegatesToアノテーションを解説する前に、delegateプロパティについて補足しておきます。クロージャにおけるdelegateプロパティとは、クロージャ中のコードにおいて、レシーバとなるオブジェクトを明示的に指定していないメソッド呼び出しやプロパティ参照における「暗黙のレシーバ」を示す動的に変更可能なプロパティです。インスタンスメソッド呼び出し「object.method()」において、「object」がレシーバです。インスタンスメソッド中のコードでレシーバを省略したときには通常メソッドが所属するクラスのインスタンスthisがレシーバとして扱われますが、それと同様にクロージャではdelegateプロパティが保持するオブジェクトを暗黙のレシーバとして呼び出そうとします。要はdelegateは、クロージャにおける「this」のようなものです。

delegateは動的に定まるプロパティであるが故に静的型チェックには本来参与できないのですが、@DelegatesToアノテーションでdelegateプロパティが保持するオブジェクトの型を明示的に指定してやることで、delegateプロパティが示すオブジェクトのメソッド呼び出しやプロパティ参照について静的型チェックができるようになります。

このような言葉での説明だけでは理解しにくいと思うので、以降では例を示しながら解説します。

@DelegatesToの使用例

まず、Groovy標準のwith句と同じような効果を発揮する以下のようなメソッド「myWith」を定義することを考えてみます。本来のwith句は、Objectクラスに追加されたGDKメソッドですが、ここでは通常のメソッドとして定義します。

def myWith(Object self, Closure cl) {
  cl.delegate = self
  cl.call()
}

これは以下のようにwith句と同じように(少し違いますが)呼び出すことができ、"ABC"が表示されます。

myWith("abc") {
  println toUpperCase()
}
// ==> "ABC"が表示される

ここまでは、delegateプロパティの典型的な使用例の1つです。

さて、このコードでtoUpperCase()の綴り間違いなども検出できるようにするために、以下のように静的型チェックを適用したいとします。

import groovy.transform.*
@TypeChecked
def d() {
  myWith("abc") {
    println  toUpperCase()
  }
}

@TypeCheckedを適用するために、対象コードをメソッド定義で囲みます。しかし、上記は以下のようにエラーになってしまいます。

wyWithTest.groovy: 10: [Static type checking] - Cannot find matching method myWithCheck#toUpperCase(). Please check if the declared type is right and if the method exists.
 @ line 10, column 14.
       println  toUpperCase()
                ^
1 error

静的型チェック配下のクロージャ中ではdelegateプロパティを参照できないし、もし参照できたとしてもdelegateプロパティにString型のインスタンスが保持されることがコンパイル時には決定できないので、toUpperCase()メソッド呼び出しを静的に解決することはできません。なので、delegateに対するメソッド呼び出しやプロパティ参照を含むクロージャは、通常は静的型チェック・静的コンパイルができません。仮に、

def myWith(String self, Closure cl) {
  cl.delegate = self
  cl.call()
}

のようにmyWithの引数selfをString型に限定しても同じです。myWithの処理内容「delegateプロパティにStringインスタンスを代入する」という処理内容を、myWithの呼び出し側は知らないからです。現在のGroovyが、メソッド呼び出しをまたがっての大域的な型推論を行わないから、とも言えます。

ちなみに、以下のように通常のwithを使う場合、

import groovy.transform.*

@TypeChecked
def d() {
  "abc".with {
    println toUpperCase()
  }
}

これはコンパイルも通り実行可能であり、tooooUpperCase()のように綴り間違いをしていた時には以下のように「そのようなメソッドは無い」という静的型エラーになります。

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
with.groovy: 6: [Static type checking] - Cannot find matching method with#tooooooUpperCase(). Please check if the declared type is right and if the method exists.
 @ line 6, column 13.
       println tooooooUpperCase()
               ^

1 error

これが可能なのは、Groovyの静的型チェッカがwithメソッドの呼び出しを特別扱いし、delegateプロパティの値がwithメソッドの対象メソッドの型であると型推論して処理しているからです。

同種のことを、ユーザ定義メソッドや、サードパーティ提供のライブラリやフレームワークなどのAPIでもできるようにすることが、@DelegatesToの存在意義の1つです。myWith定義を例にすると、以下のようにクロージャ引数clに@DelegatesToを指定します。

import groovy.transform.*

def myWith(Object self, @DelegatesTo(String) Closure cl) {
  cl.delegate = self
  cl.call()
}

@TypeChecked
def b() {
  myWith("abc") {
      println  toUpperCase()
  }
}

このように「このメソッド呼び出しに即値の引数として与えられたクロージャのdelegateプロパティにはString型インスタンスが割り当てられる」ということをコンパイラに伝えることで、クロージャ中のdelegateに対するメソッド呼び出しの静的型チェックや静的コンパイルを可能とします。なお、クロージャが即値ではなく例えば変数などに代入されたクロージャを渡すようなときには、@DelegatesToは機能しません。このことは原理的に明らかでしょう。

@DelegatesToの使用例2

delegateプロパティは、ExpandoMetaClass(EMC)を用いてクラスに動的にメソッドを追加する際にも良く使われます。この際にメソッドとして追加するクロージャを静的コンパイル・静的型チェックするのにも@DelegatesToを使用することができます。以下は、Stringにhello()メソッドを動的に追加する例です。

import groovy.transform.*

def addMethodToString(name, @DelegatesTo(String) Closure cl) {  // 引数クロージャclのdelegateプロパティにはString型が与えられると宣言
  String.metaClass."$name" = cl
}

@CompileStatic
def c() {
  addMethodToString("hello") {
    println toUpperCase() // toUpperCase()の暗黙のレシーバの型を解決でき、静的コンパイルも可能
  }
}
c()

"def".hello()

EMCを用いるためのmetaClassプロパティ自体は静的型チェック配下では使用できませんが、静的型チェックを適用しないメソッドを介してクロージャを渡すことで静的コンパイルしたクロージャを既存クラスにメソッドとして追加することができます。とはいえ、このように動的に追加したメソッドは、静的コードからは呼び出せませんので、上記ができることの有用性にはちょっとした疑問が残るかもしれません。

@DelegatesToの利用目的

@DelegatesToが使われるシチュエーション、すなわちdelegateに指定された固定クラスに対するメソッド呼び出しは、SpockやGradleなどでも使われているそうです(参考[3])。なのでこれらのツールで将来的に@DelegatesToが使用されるようになることで、これらのツールの入力となるDSLについて静的型チェックがより有効に活用され、論理的なエラーがより早い段階で検出でき、またより的確になるのかもしれません(ただし、静的型チェックに相当することをAST変換で実装することは原理的に可能なので、同等のことをこれらのツールが既に行なっていないと仮定して、の話です)。また、@DelegatesToは以下を目的として使えるとのことです(参考[1])。

  • APIのドキュメントとして
  • IDEでの補完など

IDEのDSLの補完のための情報は、従来はIDE独自の設定ファイル(EclipseのDSLディスクリプタIDEA IntelliJのGDSL)として提供されてきましたが、これらを一部置きかえ得るものでもあるようです。

型チェックを拡張し、カスタマイズする

Groovy 2.1.0では、静的型チェックの振舞いをGroovy利用者がカスタマイズすることができるようになりました。具体的には、以下のように静的型チェックのための@TypeCheckedアノテーションに、型チェックを行うためのスクリプトを指定できるようになりました。

@TypeChecked(extensions='MyExtension.groovy') // 型チェックを行うスクリプト'MyExtension.groovy'を指定
void exec()
{
  "test".toUpperCase()
}

上記のMyExtension.groovyは、Type Checking DSLと呼ばれる特別な記法で記述されたGroovyコードであり、コンパイルしておく必要はありません。留意点としては、このType Cheking DSLスクリプトは@TypeCheckedを指定したクラスやスクリプトがあるディレクトリから相対的な位置に置き、相対指定のファイル名で指定する必要があるようです(試した限りでは)。

Type Checking DSLは以下のような記法です(参考[2]より)。

onMethodSelection { expr, method -> ... }
afterMethodCall { mc -> ... }
unresolvedVariable { var -> .. }
incompatibleAssignment { receiver, name, argList, argTypes, call -> .. }

これらは、Groovy標準の静的型チェッカから呼び出されるイベントハンドラの定義であり、静的型チェックの各状況下で呼び出されます。これらのハンドラからの返り値を指定することで、デフォルトの静的型チェッカであれば報告するはずであった静的型エラーを握り潰す(=静的型エラーを静的型エラー扱いしない)こともできます。以下は指定できる静的型チェックイベントハンドラの一覧です。

  • onMethodSelection
  • afterMethodCall
  • beforeMethodCall
  • unresolvedVariable
  • unresolvedProperty
  • unresolvedAttribute
  • methodNotFound
  • afterVisitMethod
  • beforeVisitMethod
  • afterVisitClass
  • beforeVisitClass
  • incompatibleAssignment
  • setup
  • finish

Type Checking DSLのイベントハンドラで書ける処理の詳細はここでは説明しませんが、AST情報を駆使して自由にロジックを組むことができます。テストコードを見る限り、型チェック拡張によって、例えば以下のような機能が実現できそうです。

  • 存在しないメソッド呼び出しの静的型チェック時に@Grab指定を追加し、拡張モジュールを実行時に導入させる。そして拡張モジュールが追加したメソッドによって、実行は成功する。
  • sprintfでのフォーマット指定と、実引数の型チェック
  • メソッド呼び出しを大文字小文字を無視して比較するようにする
  • @DelegatesTo相当の機能を実行する。つまりDelegatesToで型チェックさせる。

Type Checking DSLの詳細を調べたい場合は、今のところGroovyのソースコードに含まれるテストコードおよびorg.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupportクラスのソースコードを読むのが最善です。

なお、「型チェック拡張がコード生成に介在できるか」が興味深いところです。もしもそれが可能なら、例えば、動的メソッドの呼び出しやプロパティ参照の静的型エラー時に、その呼び出しコードをinvokeMethod()やgetProperty()の呼び出しに置き替えることにより、Groovy++のmixed modeコンパイルに似たことができるなど、応用範囲が広くなるからです。ということでGroovy Userのメーリングリストで聞いてみたところ、静的Groovy開発者のCédric Champeau氏から回答頂き、「型チェック拡張は「型チェック」なので本来はASTの書き換えをするものじゃないけども、技術的には可能」とのことでした。また、コーディング規約の統一のためのチェッカやCodeNarcのような静的解析など汎用的なチェックに使用することも狙っているとのことでした。

型チェック拡張の意義

参考[1]では、

著者の私見では、「静的型チェック」だけではメリットが十分ではないので、次節で紹介する、性能向上をメリットとする「静的コンパイル」を通じて利用することが主となるでしょう。

などと書いてしまいましたが、型チェック拡張によるカスタマイズができることを前提とすれば、「静的型チェックを行うDSLの容易な実現」という目的においては、@CompileStaticによる性能向上が無くとも、DSLに対してより的確なコンパイル時のエラーメッセージが出せたり、IDEサポートが得られるようになるなら、静的型チェックの有用性はとても高いと言えます。しかしその場合でも、Groovyの一般利用者から見ると、直接@TypeCheckedや型チェック拡張を使用するのではなく、Gradleなどのツールやフレームワークがそれを使用し、間接的に利点が享受される、という形になるのかもしれません。

コンパイラ設定スクリプト

次にコンパイラ設定スクリプトについて説明します。コンパイラ設定スクリプトは、groovycコマンドなどのコマンドラインオプションの一つとして「コンパイラ設定」を指定できるというものです。コンパイラ設定とは、具体的にはorg.codehaus.groovy.control.CompilerConfigurationクラスのことであり、従来から明示的にこのクラスを使うことで、任意のコンパイラ設定のもとでGroovyShellなどを呼び出したりすることはできました。Groovy 2.1.0以降では、そのようなプログラムを書かなくとも手軽にコマンドラインからコンパイラ設定を指定することができるようになります。

コンパイラ設定では、任意のクラスをimport/static importした扱いにできたりなど様々な設定ができますが、特に「コンパイル対象となるスクリプトやクラスに任意のAST変換を適用する」という設定を行うことができるので有用です。例えば、コンパイラ設定スクリプトで@CompileStaticや@TypeCheckedなどのAST変換を適用することで、コンパイル対象全部に対して静的コンパイルや静的チェックを行うことができます。あるいは、全クラスに@ToStringを適用し、デフォルトの動作としてtoString()メソッドを自動生成させる、といったこともできます。

コンパイラ設定は、コンパイラ設定スクリプト(以下ではcompConfig.groovy)と呼ばれるGroovyスクリプトによる設定ファイルとして準備し、以下のようにコマンドラインオプション-configscriptでそのファイル名を指定します。

groovyc -configscript compConfig.groovy Test.sgroovy

コンパイラ設定スクリプトの記述例は以下のとおりです。

import groovy.transform.CompileStatic
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer

configuration.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic))

なお、Groovy 2.1.0-rc-1時点ではコンパイラ指定スクリプトで指定できるのはgroovycコマンドのみですが、2.1.0-finalまでにgroovyコマンドでも対応する予定とのことです(あくまで予定)。

コンパイラ設定ビルダ

さて最後に、コンパイラ設定ビルダについてですが、これは前節でのコンパイラ設定スクリプト(もしくは通常のプログラム中でのコンパイラ設定)を、より簡潔に記述するためのDSL記法です。Groovy 2.1.0では、設定ファイルはなんでもかんでもDSLにする方針のようですね。前項での「全ファイルに対して@CompileStaticを適用する」というコンパイラ設定スクリプトは、コンパイラ設定ビルダを用いて以下のように記述できます。

import groovy.transform.*

withConfig(configuration) {
   ast(CompileStatic)
}

さらに、コンパイラ設定ビルダでは、例えば拡張子が「.sgroovy」であるようなファイルのみを静的コンパイルする、といった指定も以下のように簡単にできます。

import groovy.transform.*

withConfig(configuration) {
   source(extension: 'sgroovy') {
      ast(CompileStatic)
   }
}

他にもファイル名のベース名や拡張子などで柔軟かつ一括の設定を行なうことができます。コンパイラ設定ビルダ記法の例については現時点で非常に情報が少ないのですが、org.codehaus.groovy.control.customizers.builder.SourceAwareCustomizerFactoryクラスの冒頭コメントやorg.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilderのソースコードがかろうじて参考になるでしょう。逆に言うとソースコード以外の情報は見付けられませんでした。

コンパイラ設定ビルダ及びコンパイラ設定スクリプトは、特に静的Groovyに限定された機能ではありませんが、上の@CompileStaticやもしくは@TypeCheckedを全体に適用することが有用であるため今回紹介しました。

まとめ

今回紹介した機能群によって、Groovy 2.0で登場した静的Groovy機能が、「Javaのように書ける」といったある意味後ろ向きな目的だけではなく、「より有用なDSLを書く」ということにも寄与するようになってきました。DSLや動的なGroovyコードにおいても静的型チェックが(実装が容易に)できることは有用でしょう。このような、「動的言語・動的型言語をあくまでもベースにした上での静的型の有用性の回収」はGroovyのユニークな特長であり、今後のGroovyの進化の方向性の一つでもあると思います。今後のGradleやGrailsなどのツールの対応に期待です。

参考

G*Magazine Vol.6

Groovy臨機応変(第一回) 〜動中の静…Groovy 2.1.0の新機能その1〜
アプリケーションをGroovyでコントロールする
オレオレ・プログラミング GROOVY
『Yokohama.groovy』について 〜活動報告など〜
Grails Plugin探訪 〜第7回 Remote Methodsプラグイン〜
ぐるーびーたん 第6話
リリース情報 2013.02.19