他言語からの訪問 【第 2 回】 Groovy (後編)

書いた人:上原潤二 (NTT ソフトウェア/JGGUG)

はじめに

Rubyist の皆さんこんにちは。間が空きましたが、「他言語からの訪問 【第 1 回】 Groovy (前編)」の記事を書いた上原です。 前回の記事では、予告だった書籍プログラミング Groovy も無事発刊されました。

今回は「他言語からの訪問 【第 2 回】 Groovy (後編)」をお屆け致します。

おさらい

前回は、Groovy に関して、主に以下のような内容について解説しました。

  • Groovy の特徴
    • Groovy は Ruby から多大な影響を受けた言語である。
    • Ruby が重視するコンセプトを JVM 上の言語として実装しつつ、Java との親和性を文法面および動作機構として最大限重視。大クラス主義を採用したライブラリを Java API を拡張する形で構築している。
  • Groovy の機能紹介
    • AST 変換: コンパイラ中でのコードの中間表現である AST (抽象構文木) を操作する
    • オプショナルタイピング: 任意の型指定 (実態としては型宣言は静的で型チェックは実行時に行う) 。

今回は、実行時メタプログラミング、ビルダー等について紹介します。 なお、これらにおいて「この機能があるから/無いから/違っているから、どちらの言語が優れている」というようなことを主張したいわけでは (もちろん) なく、 大きく言って類似した言語である Groovy と Ruby の間で、言語設計の相違が興味深く表われているかどうかをポイントとして (主観で) トピックを選んでみました。

対象読者

  • Ruby だけを知っていて、他の言語もそろそろ勉強したいなーと思っている人
  • Ruby に新規機能を追加したいなと考えていて何かネタがないかなーと探している人
  • Ruby での開発から Java での開発にジョブチェンジして、Java の冗長さ、厳格さなどにうんざりしている人

バージョン

本記事で記述する対象の Groovy のバージョンは 1.8.x です。また一部 2.0 (現状β) の機能についても言及します。

実行時メタプログラミング

Groovy の実行時メタプログラミング機構1のいくつかについて紹介します。ここで「メタプログラミング」という用語はクラスやオブジェクトインスタンスにメソッドやプロパティを追加する仕組みという意味で使用します。

カテゴリ

Groovy の「カテゴリ」は、クラスにメソッドを動的に追加する仕組みの 1 つです2。一般に、Ruby のオープンクラスでもそうですが、既存のクラスに対するメソッド追加は強力である反面、危険も伴います。たとえば、由来の異なるライブラリを組み合せて使用する場合に、追加するメソッドが衝突する可能性もあるからです。

カテゴリは、限定した範囲でのみメソッド追加が有効となる3ので、このような問題の発生確率を低めることができます。 カテゴリを利用するには、まず準備として、追加したいメソッドを static メソッド (Ruby で言うクラスメソッド) として定義したクラスを準備します4。このようなクラスをここではカテゴリクラスと呼ぶことにします。以下は例です。

class HelloCategory {
  static hello(String self) { // String クラスに追加しようとするメソッド。
    println "hello $self"
  }
}

ここでは、Groovy の標準クラスである String クラスに、インスタンスメソッドとして hello を追加します。 内容は、「”hello “+String の内容」を表示するというものです。

カテゴリクラスで定義するメソッドは以下のようにします5

  • static メソッドとする
  • 第一引数は、追加したいクラスの型 (ここでは String 型) としておく。この引数には、インスタンスメソッドとして呼び出されたときのインスタンス自身を参照する値が設定されて呼び出される (名前は任意だが self という名前にしておくのが良い)
  • 第二引数以降は、もしメソッドが引数をとるならその引数を定義
  • 処理として追加したいメソッドの処理を書く

カテゴリクラスには、追加したいメソッドを複数定義しておけます。 上の様に定義された HelloCategory で実際に String クラスにメソッドを追加するには以下のように use 文を使用します。

use (HelloCategory) {
  "world".hello() // => "hello world" が表示される
}
"world".hello() // => 例外: No signature of method: java.lang.String.hello()

このとき、use 文のブロック内、およびそこから呼びだしたメソッドでは hello を呼び出すことができますが、use 文を抜けると呼び出せなくなります。

EMC (ExpandoMetaClass)

クラスにメソッドを追加する他の方法、ExpandoMetaClass (EMC) を紹介します。

EMC では metaClass プロパティを使用します。Groovy から見ると、Java のクラスも含めてすべてのクラスは、metaClass というプロパティを持ちます。このプロパティの子プロパティにクロージャを代入することでクラスにメソッド追加を行うことができます。例えば、String クラスに hello メソッドを追加するには以下のようにします。

String.metaClass.hello = { println "hello $delegate" }
"ABC".hello() // "hello ABC"が表示される

「delegate」については後述しますが、EMC においては this (Ruby で言う self) のように機能する特殊なプロパティです。

上記では、クラスにメソッドを追加していますが、以下のように特定のインスタンスに対してメソッドを追加することもできます (Ruby の特異メソッド相当) 。

String s = "ABC"
s.metaClass.hello = { println "hello $delegate" }
s.hello() // "hello ABC" が表示される
"DEF".hello() // 例外: No signature of method: java.lang.String.hello()

methodMissing と invokeMethod

Ruby の method_missing の様に、存在しないメソッドを呼び出したときの動作を定義することもでき、Groovy ではまんま「methodMissing」です。継承したクラスで methodMissing を再定義することもできますが、前述の EMC を使って既存クラスに methodMissing を差し込むこともできます。

String.metaClass.methodMissing = { String methodName, args ->
  println "method $delegate.$methodName($args) called"
}
"ABC".harehoe() // 「method ABC.harehoe([]) called」が表示される

なお、ご存知の様に、methodMissing が呼び出されるのは、その名の通りメソッドが存在しない場合のみで、既存のメソッドの意味を上書きすることはありません。

しかし、Groovy では「invokeMethod」というものもあります。invokeMethod を定義すると、メソッドが存在していようといまいとこちらが呼び出され、本来のメソッドは呼び出されなくなります。Groovy でのメタプログラミングでは、invokeMethod のトラップは多用されます6

BigDecimal.metaClass.invokeMethod = { String methodName, args ->
   "method $delegate.$methodName($args) called"
}
println 3.3 + 4.5
// 「method 3.3.plus([4.5]) called」 が表示される

上では BigDecimal クラスに invokeMethod を追加してメソッド呼び出しをトラップしています。ちなみにこの実行結果を見ると、+ 演算子では plus メソッドが呼び出されることがわかります。

MOP

前項で invokeMethod, methodMissing を説明しましたが、これらのメソッドを含む「オブジェクトの振舞い」を実現するメソッド群は、MetaObjectProtocol インターフェース (MOP) で定義されており、これを実装するメタクラス7を定義してクラスの metaClass プロパティに設定することでオブジェクトの振舞いをメタレベルで変更することができます。MetaObjectProtocol で定義されているメソッドは以下の通りです。

  • getAttribute
  • getMetaMethod
  • getMetaProperty
  • getMethods
  • getProperties
  • getProperty
  • getStaticMetaMethod
  • getTheClass
  • hasProperty
  • invokeConstructor
  • invokeMethod
  • invokeStaticMethod
  • respondsTo
  • setAttribute
  • setProperty

なお、前述のカテゴリや EMC も MOP を通じて実装されています。

ちなみに、メタクラス (含 EMC やカテゴリ) によるメソッド追加は、Groovy コードの動的メソッドディスパッチの仕組みの中で実現されており、Groovy で書かれたコードからはクラスにメソッドが追加されたように見えるのですが、Java で定義されたコード、例えば下位層にある Java フレームワークや Java 標準クラスライブラリからは見えず、それらを誤動作させるように Groovy 側から修正することはできません。また、同様の理由により Java のメソッドを削除したり上書きしたりすることもできません。つまり、Groovy は「Java の上に動的言語のレイヤを被せるもの」であり、「下位層の Java の動作に侵襲して動的にする」ことはできません。

delegate と名前解決方針 (resolveStrategy)

String に hello メソッドを追加する例では、

String.metaClass.hello = { println "hello $delegate" }

のようにクロージャ中で「delegate」というプロパティを参照していましたが、これは特別なプロパティであり、メソッドにおける this/self に相当するものです。修飾のないメソッド呼び出しや変数/プロパティ参照は delegate プロパティの差すオブジェクトのコンテキストからも解決しようとします。

詳しく言うと、クロージャ中での名前解決は、

  • クロージャがレキシカルに定義されているオブジェクト (OWNER)
  • delegate で指定されたオブジェクト (DELEGATE)

の 2 つのコンテキストのいずれかもしくは両方から、resolveStrategy プロパティに設定された以下の 4 種類の方針のうちいずれかで解決されます。

DELEGATE_FIRST
delegate → owner の順
DELEGATE_ONLY
delegate のみ
OWNER_FIRST
owner → delegate の順
OWNER_ONLY
owner のみ

ビルダー

Groovy のビルダーについて解説してみます。

Ruby でも同様だと思いますが、Groovy のビルダーはクロージャと前述の invokeMethod、delegate の仕組みを使って、一連の__メソッドの呼び出しの構文でツリー構造を構築__しようというものです。

「メソッド呼び出しでツリー構造の構築」といっても、makeNode とか makeLeaf のような明示的なツリー構築メソッドの呼び出しが表面には出てこないのがミソです。X というノードを構築する処理を、X() というメソッドを 呼び出すことで代替します。

百聞は一見にしかず、ということで以下にビルダーの一例である「MarkupBuilder」の使用例を示します。 MarkupBuilder を使うと、HTML や XML などのマークアップ言語のツリー構造を、DOM などを使うよりも簡潔に、XML 自体を使うよりも簡潔に記述することができます8

import groovy.xml.MarkupBuilder
new MarkupBuilder().root {
  book isbn:12345, {
    title "我輩は猫である"
    author "夏目漱石"
  }
}
// 以下が表示される
// <root>
//  <book isbn='12345'>
//   <title>我輩は猫である</title>
//   <author>夏目漱石</author>
//  </book>
// </root>

以下のようにループ文と混在することも可能です。

import groovy.xml.MarkupBuilder
titles = ["夏目漱石全集1", "夏目漱石全集2", "夏目漱石全集3"]
new MarkupBuilder().root {
  titles.each { t ->
    book {
      title t
      author "夏目漱石"
    }
  }
}
// 以下が表示される
//<root>
//  <book>
//    <title>夏目漱石全集1</title>
//    <author>夏目漱石</author>
//  </book>
//  <book>
//    <title>夏目漱石全集2</title>
//    <author>夏目漱石</author>
//  </book>
//  <book>
//    <title>夏目漱石全集3</title>
//    <author>夏目漱石</author>
//  </book>
//</root>

Ruby でも Nokogiri ライブラリがマークアップ言語についての Builder 機能を提供しており、基本は同じなのではないかと思います。

散発的トピックス

さて、今回記事を書いてきて思ったのは、Groovy は、Builder や DSL を実装するのに都合よく作ってあるなあということです。もちろん Ruby もそうですね。以降、その他、Builder もしくは DSL を実現するのに便利な機能を落穂拾い的に紹介してみます。

識別子のクォーティング

Groovy では識別子をクォートすることで、たとえばハイフンや/、!、空白など通常識別子名ではない文字も含めることができます(Ruby でいう p:’‘、p:”” の記法) 9‘。

def 'if'(cond, Closure c) {
  if (cond) c.call()
}
'if'(true){println "true"}

なので、たとえば以下のようにハイフンを含む XML タグでも

import groovy.xml.MarkupBuilder
new MarkupBuilder().root {
  'book' {
    'title-of-book' "タイトル"  // ハイフンを含む
    'if' "予約語もオッケー"
  }
}
<root>
  <book>
    <title-of-book>タイトル</title-of-book>
    <if>予約語もオッケー</if>
  </book-info>
</root>

のように割とシンプルに記述できます。例は示しませんが、識別子をダブルクォートで括れば $ で変数や式の展開も行なわれます。

コンテキスト依存予約語

Groovy の予約語のいくつかはコンテキスト (具体的にはプロパティ参照に使われる場合) によってはただの識別子となります。

hash = [:]         // ハッシュマップ hash を準備
hash.key = "value" // 識別子はもともとクォートせずにキーとして利用可能
hash.if = 3        // 予約語であっても良い
println hash.if    // => 3 が出力される

拡張コマンド式

Groovy では、トップレベルの式文もしくはトップレベルの代入式の右辺に限定されるのですが、メソッド呼び出しのピリオドや、メソッド・プロパティ参照のピリオドが省略できる場合があります。

 def x = a(1).b(1,2,3)
 def y = a(1).b(1,2,3).c

はそれぞれ以下のように書けます。

 def x = a 1 b 1,2,3
 def y = a 1 b 1,2,3 c

この機能を拡張コマンド式と呼びます。空白で区切られたシーケンスは「メソッド名 引数 メソッド名 引数… (最後のプロパティ) 」のようにメソッド名と引数のペアと解釈するということです。

おわりに

以上で今回の記事は終りです。散発的でしたが、Builder を軸に動的な機構について眺めてみました。 私は Ruby に詳しくはありませんが、調べるにつけ、興味深く、同種の機能でも様々な違いがあるものだなあと思いましたが、みなさまはどのような感想をお持ちになりましたでしょうか。自分が言語作るならこう設計するぞ!とかの刺激になれば無上の幸いです。

さて、前回の記事執筆時点 (2011 年 6 月) は、Groovy 1.8 が正式公開された頃でしたが、現在 (2012 年 1 月) では安定版として 1.8.5 までバージョンが進んでおります。また、次期開発バージョンである (Groovy 1.9 あらため) Groovy 2.0 の開発も活発で、前回の記事でも言及した「静的 Groovy」たる G++ の機能の一部が、静的コンパイル静的型チェックとして正式に再実装されようとしてます。「それはひょっとしたら道を踏み外してるんじゃないか」というハラハラ感もあり目を離せません。

最後になりましたが、るびま編集部におかれましては貴重な執筆の機会を頂きまして、またアドバイス・ご教示頂きましてたいへんありがとうございました。たいへん勉強になりました。 るびま読者のみなさま、Ruby コミュニティの皆様におかれましては、どこかでお会いできる日を楽しみにしてます。

著者について

上原潤二

NTT ソフトウェア株式会社所属。JGGUG ( 日本 Grails/Groovy ユーザ会) 運営 委員。Java 技術および言語処理系実装に興味がある。おもな著書は「Grails 徹底入門」「プログラミング Groovy」 (共著) 。Groovy 技術に関するブログ「Gr な日々」を主宰している。Groovy の起動を高速化する OSS ソフトウェアである GroovyServ の開発者の 1 人。Groovy で書かれた Java VM “GVM” や、Groovy 上の DSL として実装された Lisp 処理系である Lisp Builder の作者でもある。

して任意の識別子が使えるはずだったが、なぜか正式版では削除された

  1. 「実行時」メタプログラミングとわざわざことわっているのは、前回説明した AST 変換が「コンパイル時」メタプログラミングだからです。groovy には事前コンパイラとして「groovyc コマンド」があり Java の仮想マシンコード (class ファイル) を事前に生成しておくことができますが、AST 変換はコンパイル時に解決されます。 

  2. Objective-C 由来の機能とのことです。 

  3. Ruby で採用が検討されている「Refinement」にも似ているようだが Refinement がレキシカルスコープであることを特徴とするのに対して、カテゴリ (use) は動的スコープ。 

  4. Java 標準 API に対する Groovy 追加メソッド GDK (Groovy JDK) は、内部的にはカテゴリと等しい仕組みで実現されている。ただ、GDK は、Groovy コードから見るとコードの実行開始時には既にメソッドが追加されているし、プログラマからは与りしらない内部機構なので「動的メソッド追加」や「実行時メタプログラミング」には当らない。 

  5. インスタンスメソッドを以下の条件で変形して定義するということだが、これはちょっと不自然にも感じられる。なのでそのままインスタンスメソッドとして定義できるようにするための、@Category という AST 変換もある。 

  6. 印象ですが、Ruby では send を置き変えることはそれほど普通に行なわれることはないように思いますが何故なのでしょう。ブランクスレートクラスを使用する場合と比較したときの考察などをご教示頂けるとありがたいです。 

  7. これを「メタ」クラスというと違和感があるかもしれません。意味的には Java の Class クラスに比して動的な側面を含むクラスオブジェクトです。 

  8. ひとことでいうと haml みたいなものを Groovy の内部 DSL として実現しようということです。 

  9. 本家 Java7 でも、異国的識別子 (exotic identifier) と