Groovy 入門
2009.3.19 - 2012.2.3 (鈴)2009.4.16 時点のページ
- 1. はじめに
- 2. てほどきとインストール
- 3. 30 分で分かる Groovy 速成講座
- 4. もっとスクリプト言語らしく
- 5. クロージャ
- 6. おわりに
- 7. 参考文献
- 付録 1. Emacs 編集モード
- 付録 2. NetBeans プラグイン
- Groovy 応用: 遅延評価によるフィボナッチ数の計算 2009.4.8
- Groovy 応用: Struts への透過的な適用 2011.3.23
1. はじめに
Groovy [codehaus.org] は主に Ruby [ruby-lang.org] に強くインスパイアされた Java の一方言 である。 その処理系は標準的な Java VM 上で動作し,標準的な Java 言語によるプログラムと同じバイナリ形式 (class ファイル) をとり, 両者を透過的に組み合わせることができる。 Groovy は既存の Java システムとの高度な互換性と,Python や Ruby に代表される,いわゆる軽量言語の利点を両立させている。
本稿は,Java の知識を前提として Groovy 言語の概要を紹介する。 Ruby の知識は必須ではないが,知っていれば,より Groovy を理解しやすいだろう。
実際,"groovy" という名前は,典型的な Unix 環境では見かけの長さ以上に悪い選択です。 例えば pyt で始まるコマンドは普通ほかにありませんから,同じだけ長い名前ながら,このつづりで 始まる python は,はじめの3文字を打鍵して補完するだけで起動できます。 しかるに groovy は groups や groff と区別するために groo まで打鍵して補完しなければなりません。 毎回フルスペルを打鍵するのと五十歩百歩です。 対話シェルを動かそうと思ったら,いったん groovy まで打鍵または補完展開した後,さらに sh と打鍵する必要があります。ほとんどいやがらせの域です。:-( あくまでこの名前にこだわるならば,JavaScript に対する js のように短い略称を本家筋で一つだけ用意しておくべきだったでしょう)
またこの単語からは,これが Java の1方言であり,Java をかじった人なら即座に書き始めることができ,既存の Java システムに簡単に組み入れることができる,熟考された良い意味での大人の言語であることが伝わってきません。 自らを groovy (イカす,かっこいい,キマッてる) と名乗る行為からは,どちらかといえば,その反対の (あえて言えば,いわゆる中二的な) 印象を受けます。
それでもこのチュートリアルを書いて Groovy への入門を君にすすめているのは,それに見合うだけの良さが,この言語と処理系にあるからです。 ほとんど失敗に近い名前のことは,たとえそれで操作にイライラさせられるとしても,印象が良くないとしても,しばらく我慢してください。
2. てほどきとインストール
ここでは Unix 環境 (Mac OS X) での操作を説明する。 純粋に Java で作られたプログラムだから,他のプラットフォームでも基本は変わらない。
2.1 対話シェルを動かそう
まず Groovy - Download [codehaus.org] から Groovy の Binary Release をダウンロードしよう。 2012年 2月 3日現在の最新安定版は 2011年 12月 23日にリリースされた Groovy 1.8.5 の groovy-binary-1.8.5.zip である。
次にこの groovy-binary-1.8.5.zip を展開すると groovy-1.8.5 というディレクトリが作られる。 その下の bin ディレクトリに移動する。 Groovy のインタープリタやコンパイラの起動スクリプトがある。 とりあえず groovysh を起動しよう。 これは Groovy 言語を対話的に解釈し,実行するシェル (shell) である。
$ unzip ~/Downloads/groovy-binary-1.8.5.zip Archive: /Users/suzuki/Downloads/groovy-binary-1.8.5.zip The Groovy 1.8.5 binary distribution. creating: groovy-1.8.5/ creating: groovy-1.8.5/bin/ inflating: groovy-1.8.5/bin/grape inflating: groovy-1.8.5/bin/groovy inflating: groovy-1.8.5/bin/groovyConsole inflating: groovy-1.8.5/bin/groovyc inflating: groovy-1.8.5/bin/groovydoc inflating: groovy-1.8.5/bin/groovysh inflating: groovy-1.8.5/bin/java2groovy inflating: groovy-1.8.5/bin/grape.bat inflating: groovy-1.8.5/bin/groovy.bat inflating: groovy-1.8.5/bin/groovy.icns inflating: groovy-1.8.5/bin/groovy.ico inflating: groovy-1.8.5/bin/groovyConsole.bat inflating: groovy-1.8.5/bin/groovyc.bat inflating: groovy-1.8.5/bin/groovydoc.bat inflating: groovy-1.8.5/bin/groovysh.bat inflating: groovy-1.8.5/bin/java2groovy.bat inflating: groovy-1.8.5/bin/startGroovy inflating: groovy-1.8.5/bin/startGroovy.bat creating: groovy-1.8.5/conf/ creating: groovy-1.8.5/embeddable/ creating: groovy-1.8.5/lib/ inflating: groovy-1.8.5/ANTLR-LICENSE.txt inflating: groovy-1.8.5/ASM-LICENSE.txt inflating: groovy-1.8.5/CLI-LICENSE.txt inflating: groovy-1.8.5/JSR223-LICENSE.txt inflating: groovy-1.8.5/LICENSE.txt inflating: groovy-1.8.5/NOTICE.txt inflating: groovy-1.8.5/conf/groovy-starter.conf inflating: groovy-1.8.5/embeddable/groovy-all-1.8.5.jar inflating: groovy-1.8.5/lib/ant-1.8.2.jar inflating: groovy-1.8.5/lib/ant-antlr-1.8.2.jar inflating: groovy-1.8.5/lib/ant-junit-1.8.2.jar inflating: groovy-1.8.5/lib/ant-launcher-1.8.2.jar inflating: groovy-1.8.5/lib/antlr-2.7.7.jar inflating: groovy-1.8.5/lib/asm-3.2.jar inflating: groovy-1.8.5/lib/asm-analysis-3.2.jar inflating: groovy-1.8.5/lib/asm-commons-3.2.jar inflating: groovy-1.8.5/lib/asm-tree-3.2.jar inflating: groovy-1.8.5/lib/asm-util-3.2.jar inflating: groovy-1.8.5/lib/bsf-2.4.0.jar inflating: groovy-1.8.5/lib/commons-cli-1.2.jar inflating: groovy-1.8.5/lib/commons-logging-1.1.1.jar inflating: groovy-1.8.5/lib/extra166y-1.7.0.jar inflating: groovy-1.8.5/lib/gpars-0.12.jar inflating: groovy-1.8.5/lib/groovy-1.8.5.jar inflating: groovy-1.8.5/lib/hamcrest-core-1.1.jar inflating: groovy-1.8.5/lib/ivy-2.2.0.jar inflating: groovy-1.8.5/lib/jansi-1.7.jar inflating: groovy-1.8.5/lib/jline-0.9.94.jar inflating: groovy-1.8.5/lib/jsp-api-2.0.jar inflating: groovy-1.8.5/lib/jsr166y-1.7.0.jar inflating: groovy-1.8.5/lib/junit-4.10.jar inflating: groovy-1.8.5/lib/servlet-api-2.4.jar inflating: groovy-1.8.5/lib/xmlpull-1.1.3.1.jar inflating: groovy-1.8.5/lib/xstream-1.4.1.jar $ cd groovy-1.8.5/bin $ ./groovysh Groovy Shell (1.8.5, JVM: 1.6.0_29) Type 'help' or '\h' for help. ------------------------------------------------------------------------------- groovy:000>
対話シェルでは Groovy 言語をその場で入力して応答を得ることができる。
groovy:000> 2 + 3 ===> 5 groovy:000> "hello, world" ===> hello, world groovy:000> println "hello, world" hello, world ===> null groovy:000> println() ===> null groovy:000> exit $
この例では,
- 算術式 2 + 3 を与えて,結果の数 5 を得,
- 文字列 "hello, world" を与えて,結果の文字列 "hello, world" (ただしここでは引用符は表示されていない) を得,
- 改行付き印字メソッド println を呼び出して引数を画面に表示させ改行させた後, 結果の戻り値 null を得,
- 再び println を今度は無引数で呼び出して単に改行させた後,結果の戻り値としてまた null を得ている。
これらの入力はどれも Groovy の式であり,===> の先に表示されているのは,式を評価した結果の値である。 対話シェルは,式を与えて結果の値を得る電卓として使うことができる。 文字列定数の場合は,文字列定数それ自身が結果の値である。 メソッド呼出しの場合は,戻り値が,結果の値である。 印字メソッドによる画面表示はあくまでメソッド内の動作であり,結果の値としては (この例では) null であることに注意しよう。
println とはどのクラスのメソッドなのか,については 4.3 節で扱います。
さしあたり,それまでは,なぜかどこでも使えるメソッドだ,ぐらいに思ってください。
メソッドですから this.println() などとしても OK ですが,普通は誰もそうしません。
その理由は 4.3.1 節で述べます。
Groovy 言語では,他の式の一部分ではない式,つまり最外の式に限り,引数付きメソッド呼出しの丸括弧を省略してよい。 ここでは println の最初の呼出しで丸括弧を省略している。
println("hello, world") です。
これを対話シェルに与えて,println "hello, world" と同じになることを確かめてください。
ですから,もしも
println() のつもりで println とだけ入力すると,
No such property (そのようなプロパティはない) と叱られます。
groovysh は Groovy 言語の範囲外の「コマンド」をいくつか用意している。 exit は groovysh 自身を終了させるコマンドである。 メソッドではないから exit() ではなく単に exit と入力する。 help と入力すれば他のコマンドを一覧できる。
対話シェルはコマンドと Groovy の式のほか,Groovy の文や宣言も受け付ける。 文や宣言にも結果の値がある。
一応 Groovy が動くことを確かめたら,きっちりインストールしよう。
2.2 インストールしよう
前節では,何が展開されたか確認の意味も兼ねて groovy-1.8.5/bin のディレクトリまで移動した。 実際には,わざわざそうしなくても,groovysh への相対パスまたは完全パスを与えることで groovysh を起動できる。 さらに今の版の groovysh は PATH の通った場所からシンボリック・リンクを張っただけも起動できる。 最後の方法が最も手間がかからない。ここでは,この方法を使ってインストールしよう。
下記は /usr/local/share に groovy-1.8.5 を展開し, /usr/local/bin から groovysh へのシンボリック・リンクを張る例である。
$ cd /usr/local/share $ sudo unzip ~/Downloads/groovy-binary-1.8.5.zip ……出力略…… $ cd ../bin $ sudo ln -s ../share/groovy-1.8.5/bin/groovysh .
こうした後,type コマンドと ls コマンドで次の例のように出力されるはずだ。
$ type groovysh groovysh is /usr/local/bin/groovysh $ ls -l /usr/local/bin/groovysh lrwxr-xr-x 1 root admin 34 Feb 2 10:01 /usr/local/bin/groovysh -> ../share/g roovy-1.8.5/bin/groovysh
さしあたり同様にして,非対話型インタープリタ groovy とコンパイラ groovyc のシンボリック・リンクも作ろう。
2.3 Groovy スクリプトを動かしてみよう
下記の内容のファイル hello.groovy をテキスト・エディタで作る。
println "hello, world"
コマンド groovy を使えば,これを下記のように実行できる。
$ groovy hello.groovy hello, world $
2.4 groovyc で Java クラスを作ってみよう
下記の内容のファイル Greeting.groovy をテキスト・エディタで作る。
// テスト用クラス package com.example.proj public class Greeting { public static void hello() { println "hello, world" } }
これを groovyc でコンパイルすると,Java クラス・ファイル com/example/proj/Greeting.class が得られる。
$ groovyc Greeting.groovy $
これを Java プログラムから呼び出してみよう。
下記の内容のファイル Main.java をテキスト・エディタで作る。
import com.example.proj.Greeting; public class Main { public static void main(String[] args) { Greeting.hello(); } }
同じディレクトリでコンパイルする。
$ javac Main.java $
実行するには,カレント・ディレクトリと groovy-1.8.5/embeddable/groovy-all-1.8.5.jar を CLASSPATH に含める。 この jar ファイルは groovy の処理系一式のクラス・ファイルを含んでいる。
$ java -cp ".:/usr/local/share/groovy-1.8.5/embeddable/groovy-all-1.8.5.jar" Main hello, world $
2.5 ここまでと,これから
この章で君は groovy 処理系の基本的な使い方を学んだ。 groovy は対話的に実行したり,スクリプト・ファイルを実行したり, Java のクラス・ファイルへとコンパイルできる。
一方,ここまでのプログラム例に, セミコロンを省略したり,System.out でもないのに println メソッドで画面表示したりと,微妙に Java と異なる点があったことに,とまどっているかもしれない。 しかし,これはまだ気に病むことではない。 もしもそうしたければ,Java と全く同じようにセミコロンを書いたり,System.out.println を呼び出すことができる。
groovy:000> System.out.println("hello, world");
hello, world
===> null
groovy:000>
これから,まず 第3章で,Java との共通性に注目して Groovy を学ぼう。 Groovy らしくはないかもしれない。 しかし,これだけで君はただちに正しく動く Groovy プログラムをどんどん書くことができる。 もちろん,それだけでは Groovy はインタープリタ付きの少し変わった Java 方言にすぎない。
だから,次に 第4章で,軽量言語,ないしはスクリプト言語としての Groovy の書きかたを学ぼう。 セミコロンを省略できることはその一部である。 おそらく,君は今まで断片的な Groovy の解説記事をあちこちで見てきたことだろう。 そこでは,まるで Ruby や Python のように def キーワードでメソッドを定義していたかもしれない。 そういった言語としての Groovy の姿がここで出現する。
さらに章を改めて 第5章 では,最近,様々な言語で抽象化手段として注目されているクロージャ (closure) について学ぼう。 クロージャそのものは Algol 60 にも萌芽が見られる古典的な構成要素だが,Groovy はとりわけ Ruby でのクロージャの活用法にインスパイアされている。 5.5 節 の例にあるように,表面的には Ruby の模造品にしか見えないかもしれない。 しかし,その背後にある構文と意味論は Ruby の引き写しではない。複雑さを大胆に省き,簡明なかたちに再構成している。 ある意味,ここに Groovy らしさの真髄がある。
3.30 分で分かる Groovy 速成講座
もしも君が Java を知っているならば,数千行以上の Groovy プログラムを書くために十分なだけの知識を得るのにそれほど時間はいらない。
- 妥当な Java プログラムの大半はそのまま妥当な Groovy プログラムである。
したがって Java プログラマならば,誰でもすぐに Groovy プログラミングが可能です。
- どの Java クラス・ライブラリも Groovy から利用できる。
Java と同じく,環境変数 CLASSPATH に入れて import 可能にできる。
下記はあらかじめ import 済みです。陽に import しなくても構いません。
java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.* - ファイルの接尾辞は .groovy である。
このことを除き,
妥当な Java プログラムのファイル編成は,そのまま妥当な Groovy プログラムのファイル編成である。
例えば
package com.example.projと宣言されたクラスFooのソースを, ディレクトリ com/example/proj にファイル Foo.groovy として置けば, 他の Groovy プログラムからimport com.example.proj.Fooとして import できます。ソースのかわりに,groovyc でコンパイルした Foo.class を同じ場所においても構いません。 この場合は Java プログラムからもクラス com.example.proj.Foo を import できます。 2.4 節 を見直してください。
注意すべき Java との言語仕様の相違点を次に示す。
-
プリミティブ型か参照型かにかかわらず,
a == bは値の一致を判定する (a != bは値の不一致を判定する)。
参照型のアイデンティティの一致はa.is(b)で判定する。Groovy では,a == bとa.equals(b)は,数値を比較するときに算術変換を行うかどうかだけが異なります。
equals()では 1 と 1.0 は別の値と判定されます。groovy:000> new Integer(1).equals(new Integer(1)) ===> true groovy:000> new Integer(1).equals(new Double(1.0)) ===> false
==では 1 と 1.0 は (整数を実数に変換して比較するので) 同じ値と判定されます。groovy:000> new Integer(1) == new Integer(1) ===> true groovy:000> new Integer(1) == new Double(1.0) ===> true groovy:000> 1 == 1.0 ===> true
別々のインスタンスでも==で等しいと判定されることに注意してください。 算術変換を使うことを除けば,Groovy の==はequalsメソッドと同じです。 さらに言えば,new Integer(1)あるいはInteger.valueOf(1)と書く必要は実はありません。 単に1と書いたとき,実際には Java のInteger.valueOf(1)を意味します。 - char や int などのプリミティブ型は,原則として
java.lang.Character や java.lang.Integer などのラッパー・クラスのインスタンスで表現される。
ただし,普段はこのことを意識する必要はない。
プログラムでは普通に char や int として宣言して扱うことができる。
前述のように == (および !=) はアイデンティティではなく値の一致 (および不一致) を判定する演算子として振舞います。他の演算子も,その値に対して,必要ならば算術変換を伴って演算をします。 可能ならばプリミティブ型のまま演算される場合もあります。 プリミティブ型を扱う Java のクラス・ライブラリとのあいだでは引数と戻り値が透過的に変換されます。
-
a.equals(b)とa.is(b)は レシーバ a が null のときも正しく働く。 null は事実上,1個のインスタンスとみなされる。名目上,null.equals(b)とnull.is(b)はそれぞれ null と b が同じ値かどうか,および,同じアイデンティティかどうかを判定します。 名目は異なりますが,どちらも結局,単に b が null かどうかを判定します。 Groovy の null は Java の null とたしかに同一ですが, Groovy では擬似的に org.codehaus.groovy.runtime.NullObject の唯一のインスタンス,つまりシングルトンとして扱います。 したがってnull.toString()やnull.getClass()などのメソッド呼出しもできます。Groovy の==とは算術変換を伴うequalsであるというルールが,null をインスタンス扱いすることによって,簡単になっていることに注意してください。 説明に場合分けが不要になっています。 これと同様に,実際のコーディングでも null に対する特別な場合分けの大半を省くことができます。 - 文字列定数は,単一引用符で囲んでも,二重引用符で囲んでもよい。
char 定数を得たいときは文字列から型変換して作る。
例:(char) 'a'または'a' as char
独自の演算子 as を使ったほうが,読むときに Java とは異なる型変換として認識しやすい点でいくらか好ましい。二重引用符で囲んだ場合は,Unix のシェルと同じように$記号で変数展開ができます。 例:"hello, $name"(型名) によるキャスト演算と as 演算は,基本的には同じですが,長さ 2 以上の文字列の char への変換で結果が異なります。 (型名) によるキャスト演算は例外を送出します。 as 演算は最初の文字の char 値を返します。 一般に (型名) によるキャスト演算は,異議のありそうな変換に対して,より保守的です。C# ファンへの警告: Groovy の as は C# の同名の演算子とは似て非なるものです。"abc" as java.util.Mapのように必ずしも直感的でない変換を大胆に行う場合もあれば,"abc" as Threadのようにキャスト失敗の例外を送出する場合もあります。 C# での (型名) キャストと as の使い分けは Groovy には通用しません。 - 任意の式を boolean にキャストすることができる。
このキャスト演算は (boolean 変数への代入など) キャストが必要とされる箇所で暗黙裏に行われる。
null,
0,"", 空のコレクション (例:new ArrayList()), 空の配列 (例:new int[0]) は false と見なされる。自動的にキャストが行われる典型的な場所として,if (e) …やwhile (e) …などのテスト式 e があります。 ただし,この場所にじかに代入演算をおくことは構文的に禁止されています。 こうして,同様の緩やかさをもった他の言語でよく起こってきた間違いを防止しています。 もしも本当に代入演算をおきたいときは,さらに括弧で囲みます。
不可:if (a = foo()) bar();
可:if ((a = foo())) bar();
Java では (不適切にも) 許されていた boolean 変数 flag に対するif (flag = true) …が Groovy では構文解析時のエラーとなることに注意してください。 - protected や private によるアクセス制御は,実行時に無視される。
Groovy に対してプライバシーはない。
Groovy 公式ページ [codehaus.org] は,他のオブジェクトのプライバシーの無視 (disregarding other objects' privacy) を things you can do but better leave undone (できるが,しないほうがよいこと) の一つに挙げています。 単体テスト等での利用に留めるのが妥当です。むしろ,そこでこそ極めて有用な機能になります。 テストのためだけにオブジェクト内部へのアクセス制御をわざわざ緩める必要がなくなるわけです。
アクセス制御の指定はクラスの情報としてコンパイル時に織り込まれますから, Groovy で定義したクラスを Java からアクセスするときは,この制御が有効になります。 - クラス定義,メソッド定義にアクセス制御を指定しなかったとき,public として定義される。
クラスとメソッドに対する Java 本来の無指定時のアクセス制御 (package private) は Groovy では表現できない。
したがって 2.4 節 の Greeting.groovy の public キーワードは省略できます。
- クラスのフィールド
Klass xyzにアクセス制御を指定しなかったとき, Java Beans プロパティとしてのゲッターpublic Klass getXyz()とセッターpublic void setXyz(Klass)およびフィールドprivate xyzが自動的に定義される。 フィールドに対する Java 本来の無指定時のアクセス制御 (package private) は Groovy では普通には表現できない。フィールドを package private にしたいときは,@PackageScopeアノテーションを使います。class A { @PackageScope int x; int y; }
この例では java.lang.Object の派生クラスとして public class A が定義されます。 このクラスはアノテーションで指定されたとおりの package private な int x と, JavaBeans プロパティの実体となる private int y を持ちます。 JavaBeans プロパティとしてメソッド public int getY() と public void setY(int) が自動的に定義されます。実際の Java のコーディングでは,振舞が中途半端で使い方にセンスが要求される (つまり,不適切な選択の危険が増える) ためか, pakcage private はほとんど使われません。 Java の設計者にしてみれば,気の利いた省略時解釈を用意したはずが,実際にはほとんど使われずに期待外れなのかもしれません。 Groovy は Java の現実の使われ方を踏まえ,クラスのフィールドの最も普通の定義のしかたを省略時解釈にして,この空白地帯を再利用しています。 - メソッドの throws 宣言は Groovy からは無視される。
チェックされる例外と,チェックされない例外の区別は Groovy にはない。
アクセス制御の指定と同じく,この宣言もクラスの情報として織り込まれますから,Groovy で定義したクラスを Java からアクセスするときは,この宣言が有効になります。 Groovy で定義したクラスのメソッドから例外を送出して Java で catch する場合は,忘れずに Groovy のメソッド定義で throws 宣言をしておきます。
- do … while 文を使うことはできない。
無限ループと if 文,break 文の組み合わせで代用できます。 下記は
i = 1; do { println i; i++; } while (i <= 10);の置き換えです。groovy:000> i = 1; for (;;) { println i; i++; if (i > 10) break; } 1 2 3 4 5 6 7 8 9 10 ===> nullこの例では変数 i を突然使っていますが,対話シェルと,Groovy スクリプトのクラスの外側では,変数を宣言せずに使うことができます。 とりわけ対話シェルでは,もしも変数をわざわざ宣言すると,その1行でしか変数を使えなくなりますから,普通は宣言せずに使います。 4.3.3 節で説明します 。 - 拡張 for 文では,コロンのかわりに in を使う。
例:
for (int i in someList) { … } - 値の並びをカギ括弧で囲んで java.util.ArrayList インスタンスを構築できる。
配列の初期化構文はない。
例を示します。拡張 for 文も使ってみます。
groovy:000> x = [1, 2, 3, 4, 5] ===> [1, 2, 3, 4, 5] groovy:000> x.getClass() ===> class java.util.ArrayList groovy:000> for (int i in x) println i 1 2 3 4 5 ===> null
- クラスによっては,代入時に自動的にキャスト演算が行われる。
java.lang.String 値へのキャストでは toString() メソッドが使われる。
java.util.ArrayList 値から配列変数へのキャストでは toArray メソッドに相当する演算がなされる。
配列の初期化構文はありませんが,整数配列 ii を {1, 2, 3} と初期化するには,波括弧をカギ括弧にかえて
int[] ii = [1, 2, 3]と書けば OK です。 - 変数やメソッドの型の不一致は,型変換の失敗として実行時に検出される。
メソッドはオブジェクトの動的な型に基づいて実行時にはじめて解決されるから,
未定義メソッドの呼出しも,メソッドの欠如として実行時に検出される。
例えば,下記のコードは,問題なく groovyc でコンパイルすることができます。
class A {} class B {} class C { static void main(String[] args) { A a = new B(); } }
しかし,いざ実行してみると,キャストに失敗して次の例外が発生します: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'B@1428ea' with class 'B' to class 'A'
この失敗が「型の不一致」ではなく「型変換の失敗」であることに注意してください。 Groovy は式の値を実装内部で (基本的には) java.lang.Object の静的型として扱います。 それを変数に代入するとき,Object から変数の型へとキャストします。 本来の Java ならば静的な型検査の失敗として検出される誤りが Groovy では動的な型変換の失敗になります。
プログラムの自明な誤りを早期に発見できないという意味で,これは Groovy の弱点ですが,ごく近い将来の Groovy ではオプショナルな静的型検査の導入によってこれが改善されます。 まだ正式版ではありませんが,Groovy 1.8.5 と同時にリリースされた Groovy 2.0.0 β2 では下記のように @TypeChecked アノテーションを付けることができます。import groovy.transform.TypeChecked; class A {} class B {} @TypeChecked class C { static void main(String[] args) { A a = new B(); } }
このとき次のようにコンパイル時に型の不一致が検出されます。a.groovy: 10: [Static type checking] - Cannot assign value of type B to variable of type A @ line 10, column 5. A a = new B(); ^一方,その逆に,Groovy はそもそも言語に可能な動的性質をまだ十分に極めているとはいえないきらいもあります。 特定のクラスへの暗黙の型変換を実行時の処理で実現しているのですから, その処理で利用者定義のメソッドを呼び出すようにすれば,暗黙の型変換自体を実行時に自由に変更できるはずです。 しかし,現実はそうなっていません。現在の実装では,上記class Aのような一般のクラスの変数へ代入するとき, 次のメソッドを使ってそのクラスへの暗黙の型変換が行われます:public static Object org.codehaus.groovy.runtime.ScriptBytecodeAdapter#castToType(Object, Class)
今のところ,これは作り付けの動作しかできません。 しかし,このメソッドのソースを追って行くと TODO コメントとして暗黙の型変換を利用者定義可能にする考えが語られています。 このメソッドの実装は 2.0.0 β2 でもまだ現状と変わりませんが,もしも,この考えが実現すれば,Groovy は今以上に完全な動的言語になります。 - static メソッドの本体では this として java.lang.Class インスタンスが得られる。
groovy:000> class A { groovy:001> static Object getThis() { return this; } groovy:002> } ===> true groovy:000> A.getThis() ===> class A groovy:000> A.getThis().getClass() ===> class java.lang.ClassGroovy を Java の方言として使う限り,このことを意識する必要はほとんどありません。 ただし,static メソッドの中からじかに java.lang.Class とそのスーパークラス java.lang.Object のメソッドを使える (使ってもエラーにならない) ことに注意してください。
このような static メソッドの解釈は,本来の Java 言語での static メソッドに対する synchronized 修飾と一致します。裏返して言えば Java はこの解釈を中途半端に実現しているわけです。 この意味で言えば Groovy は Java が本来そうあるはずだった姿,おおげさに言えば,内在する対称性の具体化になっています。 人によっては null のインスタンス扱いでも同じことを感じたかもしれません。 - ブロックを単体で使う場合は,適当なラベルを付ける必要がある。
L1: { String s = 0xFF; System.out.println(s); }
を実行すると,文字列型のローカル変数 s に,暗黙の型変換によって 16 進数 0xFF の Integer インスタンスの toString() の結果である "255" が代入され,255 と表示されます。 ブロックですから,実行後,ローカル変数 s はどこにも残りません。 ここでラベルL1:を省くと構文エラーになります。
ラベルのないブロックを単体で使うことは日常的には滅多にありませんから, このチュートリアルを終えた後は,そんな制限もあったとおぼろげに覚えておくだけで十分です。 この制限は,クロージャの省略記法と構文的に区別するために必要とされます。
4. もっとスクリプト言語らしく
Java を知っていれば,前章までの知識だけで,さしあたり Groovy を使うことができる。 しかし,それでは Groovy を使う意味があまりない。
Groovy にはコードをスクリプト言語らしく記述するための便宜がいろいろと用意されている。 2.1 節 では,引数付きメソッド呼出しが最外の式ならば,引数を取り囲む丸括弧を省略できることを述べた。 ここでは,そのほかのスクリプト言語らしい特徴をいくつか取り上げる。 前章で述べた JavaBeans プロパティの自動的な定義については 4.4 節 で再び取り上げる。
4.1 セミコロンと return の省略
行末のセミコロンは,あいまいさがなければ省略してよい。 すでに 2.4 節 の例などでこれを見てきた。 閉じ波括弧「}」の直前のセミコロンも省略してよい。 対話シェルで例を示す。
groovy:000> L1: { String s = 0xFF; println s }
255
===> null
groovy:000> L1: { String s = 0xFF; println s.getClass() }
class java.lang.String
===> null
一方,行末まで括弧が開いたままならば,自動的に次の行に継続する。
groovy:000> 1 + ( groovy:001> 2 + 3) ===> 6
groovy:000> 1 + groovy:001> 2 ===> 6Groovy 1.8.5 では1行目を入力した時点で
groovysh_parse: 1: expecting EOF, found '+' @ line 1, column 3. と叱られます。
ところで, 最初の単体のブロックの例では ==> の先に結果の値として null が示されている。 これは正確には何だろうか?
2.1 節で最初に対話シェルを動かしたとき,println メソッドを実行して null が結果の値として得られたことを思い出そう。 単体ブロックを実行して得られた null は,実は,そのブロックの最後に実行された println メソッドの結果の値にほかならない。 ブロックで最後に実行された文の結果の値が,ブロック全体の結果の値になる。
このことから予想されるとおり,メソッドの戻り値を指定するとき,必ずしも return 文を使う必要はない。 メソッド本体のブロックで最後に実行される文の値がそのまま,そのメソッド呼出しの結果の値になる。 戻り値を指定するためには,最後の文として単に,戻り値となる式 (厳密にいえば式文) を置けばよい。
このことを利用した階乗関数の例を下記に示す。 ここでは,桁あふれを起こさないように,戻り値の型として BigInteger を指定している。 再帰の底では int の 1 が戻り値だが,指定されている戻り値の型 BigInteger へと自動的にキャストされる。
groovy:000> BigInteger factorial(int n) {
groovy:001> if (n == 1)
groovy:002> 1
groovy:003> else
groovy:004> n * factorial(n - 1)
groovy:005> }
===> true
groovy:000> factorial(100)
===> 933262154439441526816992388562667004907159682643816214685929638952175999932
29915608941463976156518286253697920827223758251185210916864000000000000000000000
000
4.1.1 クラスの外側の関数
Ruby や Python では日常のことなので違和感が無かったかもしれないが,Groovy の対話シェルで上記の階乗関数 factorial(int n) のようにクラスで囲まずに関数を定義できることに改めて注意しよう。 2.3 節で試したような Groovy スクリプトでも,このように関数を定義できる。
一見すると,こうして定義した「関数」は,クラスとは無関係のように見える。 しかし,一歩実装に踏み込めば,これもやはりクラスに属するメソッドであることが分かる。 さらに言えば this を必要とする普通の非静的メソッドである。 この「関数」の実行時に Groovy が暗黙裏に与える this の値は,対話シェルやスクリプトの実行文脈として Groovy が自動生成する groovy.lang.Script 派生クラスのインスタンスである。 これらの実装詳細を普段から意識する必要はないが,そこから帰結される一つの重要な制限には注意する必要がある。 すなわち,
- クラスの外側で定義した関数を,クラスのメソッドの中から呼び出すことは (素直には) できない。
クラスの外側の関数は,全クラスから利用できる共通ユーティリティをかっちり書くためではなく, クラスを外側から扱うメイン・スクリプトやテスト・スクリプトの共通処理をスクリプト言語らしくさらっと書くためにある。 その限りでは,この制限は制限にならない。 しかるに,もし長期にわたり広範囲に使う共通ユーティリティを書くつもりならば,静的な妥当性検査や実行効率を考慮し,手間をかけて Java のクラスとしてがっちりと書くべきだ。 そのプロトタイプを Groovy で書いてもよい。どちらも Groovy からは区別なく利用できる。
class A { int foo(x) { bar() + x } } int bar() { 10 } a = new A () println a.foo(1) // ==> (groovy.lang.MissingMethodException 発生)というスクリプトを考えましょう。 実際にこれを実行すると,メソッド foo の中から関数 bar を見つけられずに例外が発生します。 そうならないように関数 bar を呼び出すには,bar をメソッドとして扱い,レシーバを陽に与えます。 レシーバには,bar が定義されているのと同じスクリプトの地の部分,つまり,トップレベル (4.3.2 節 参照) での this の値を与えます。 つまり,次のように書き換えます。
class A { int foo(x, self) { self.bar() + x } } int bar() { 10 } a = new A () println a.foo(1, this) // ==> 11あくまで参考のために説明しました。 実際には,このように実装詳細に土足で踏み込むようなことはなるべく避けるべきです。
4.2 型を指定しないときの def キーワード
変数を宣言したり,メソッドを定義するとき,これまでの例では,すべて Java と同じように変数の型やメソッドの戻り値の型を明示してきた。しかし,型を明示しないこともできる。 このとき,型の指定を省略して def を書く。
groovy:000> def s = 0xFF; println s; println s.getClass() 255 class java.lang.Integer ===> null
この例で def だけで宣言されたローカル変数 s は,
型を決めていないから,代入されてくる値をそのまま自らの値とする。自動的な型変換は行われない。
整数 0xFF が代入されれば,その変数値の型はそのまま java.lang.Integer になる。
ただし,少なくとも今の Groovy では,実行時の動的な型に基づいてメソッドを解決しますから,その区別は名目的なものです。 下記の例が示すように Object と宣言されていても,実行時の型に基づいて,例えば String#length() を呼び出すことができますから,Object 型だけでもほとんど間に合います。 結局,今のところ def は,プログラムを書く人にとっては便利な略記法であり,プログラムを読む人にとっては意図を読み取るヒントというわけです。
groovy:000> def s = "abc"; s.length() ===> 3 groovy:000> Object s = "abc"; s.length() ===> 3
より正確に言えば,Groovy の def は何かに名前を与えて定義 (define) するためのキーワードである。 型の指定は省略することも記述することもできる。 型を省略したときは前述のとおりである。 その一方,型を記述するときは def を省いてよい。このときは,慣れ親しんだ Java の宣言と同じ構文になる。
groovy:000> def class A {
groovy:001> def String s = "abc"
groovy:002> def String add(def String t) { s + t }
groovy:003> }
===> true
groovy:000> a = new A()
===> A@1a9bea3
groovy:000> a.s
===> abc
groovy:000> a.add("def")
===> abcdef
この点で Groovy の def は C++11 の auto と似ています。
構文から宣言であることが明らかならば,def と型の両方を省略してよい。 メソッド (関数) の仮引数の宣言と catch 節の例外パラメタがこれに該当する。
groovy:000> def foo(def n) { n + 10 }
===> true
groovy:000> foo(1)
===> 11
groovy:000> def foo(n) { n + 100 }
===> true
groovy:000> foo(1)
===> 101
groovy:000> try { 5 / 0 } catch (e) { println e }
java.lang.ArithmeticException: Division by zero
===> null
しばしば Groovy スクリプトとして紹介されるところの,メソッドやフィールドを def で定義し,仮引数並びに変数名だけを連ねているプログラムは,実はこのような省略の産物である。
ちなみに前節の factorial 関数を型を明示せずに def を使って書くと,少し大きな数に対して Integer が桁あふれを起こす。 これは演算がすべて int ないし Integer どうしに限られるためである。
groovy:000> def factorial(n) {
groovy:001> if (n == 1)
groovy:002> 1
groovy:003> else
groovy:004> n * factorial(n - 1)
groovy:005> }
===> true
groovy:000> factorial(100)
===> 0
groovy:000> factorial(5)
===> 120
桁あふれを防ぐには,再帰の底の戻り値として 1 のかわりに
BigInteger にキャストした (BigInteger) 1 あるいは視認性の点からより望ましくは
1 as BigInteger を置くか,または
Groovy の BigInteger 定数の記法を使った 1G を置けばよい。
もしくは単に引数として BigInteger 値を与えればよい。
groovy:000> factorial(100G) ===> 933262154439441526816992388562667004907159682643816214685929638952175999932 29915608941463976156518286253697920827223758251185210916864000000000000000000000 000
groovy:000> factorial(1G).getClass() ===> class java.lang.Integer
4.3 Groovy JDK
今まで println メソッドを説明せずに使ってきた。 これが何者なのか,ここで明らかにしよう。
インスタンスのクラスに対して定義されていないメソッドが呼び出されたとき,Groovy はただちには誤りとしない。 もしもあらかじめ別に用意した特定のメソッド群の中に適合するものがあれば,インスタンスと元々の引数を引き渡してそれを呼び出す。 println メソッドの実体は,このようなメソッドの一つであり,その具体的な実装は Groovy のソースの groovy-1.8.5/src/main/org/codehaus/groovy/runtime/DefaultGroovyMethods.java の下記の部分にある。
/**
* Print a value formatted Groovy style (followed by a newline) to self
* if it is a Writer, otherwise to the standard output stream.
*
* @param self any Object
* @param value the value to print
* @since 1.0
*/
public static void println(Object self, Object value) {
// we won't get here if we are a PrintWriter
if (self instanceof Writer) {
final PrintWriter pw = new GroovyPrintWriter((Writer) self);
pw.println(value);
} else {
System.out.println(InvokerHelper.toString(value));
}
}
どこかある場所で,ある変数 x を引数として println(x) を呼び出したとしよう。
このとき,その文脈での this をレシーバとして println という名前のメソッドが探索される。 this が属するクラスは java.lang.Object とは限らない。 そのクラスからメソッドの探索が始まる。 しかし,適合するメソッドがなかなか見つからないとしよう。 最後には共通基底クラス java.lang.Object で探索される。 ようやくこの public static void println(Object self, Object value) が適合する。 その呼び出し時には,仮引数 self に this が,value に x がそれぞれ引き渡される。
つまり,Groovy の中では,事実上 Java.lang.Object に1引数のメソッド public void Object#println(Object value) が追加されていると見ることができる。 第3章 で述べた boolean is(Object) メソッドも,このような「追加されたメソッド」である。 ほかにも多数のメソッドが追加されている。 JDK のクラス・ライブラリに対して「追加されたメソッド」からなる Groovy の (中にだけで通用する) ライブラリを Groovy JDK または GDK と呼ぶ。
void を戻り値の型とする Java メソッドを呼び出したとき,本来ならば戻り値はないが,Groovy は null をその便宜的な結果の値として返す。 2.1 節以来,今まで君は println の結果の値として null を見てきた。 それは上記に引用したメソッド定義の戻り値の型 void に由来している。
4.3.1 this と組込み関数
またこの println のメソッド定義から分かるように,レシーバ (ここでは仮引数 self ) は,java.io.Writer インスタンスの場合を除き,メソッドの中で無視される。 例えば,整数 1 は Groovy の中では java.lang.Integer ひいては java.lang.Object のインスタンスだから,もしそうしたければ 1.println(expression) のように呼び出すことができる。 しかし,たとえそうしてもレシーバとしての 1 は無視され,意味をもたない。
groovy:000> 1.println(3) 3 ===> null
つまり,println メソッドの呼出しでわざわざ java.io.Writer インスタンス以外のレシーバを書くことは,書くだけ無駄な労力である。 それどころか実際の処理では意味をもたないのに,あたかもその値に意味があるように見えて紛らわしいきらいがある。 だから普通は書かない。書かなければ Java と同じく this が暗黙裏にレシーバとして使われる。
this の値そのものは呼び出す文脈により異なるが,(java.io.Writer の派生クラスのコンストラクタや非静的メソッド定義の文脈を除き) どこで呼び出そうとも,どのみちそのレシーバは無視されるから,引数を印字して改行するという println の振舞は変わらない。 一般に,java.lang.Object のインスタンスをレシーバとし,実際の処理ではレシーバを無視するメソッドは,単に,どこからでも便利に呼び出せる組込み関数として使うことができる。
したがって,もしも仮にすべてのクラスの基底クラスであるような特別なクラスがあるとすれば,……(略)…… (文脈により self の値は変わるけれども) そのメソッドは Ruby のすべての文脈で呼び出せることになる。
……(略)…… メソッドの動作が,レシーバである self の値に依存していなければ,そのメソッドは,すべての文脈で同じように振舞うことになる。 つまり,そのメソッドは事実上,いわゆる普通のグローバルな関数と同じになる。これが p や puts の正体である。
クラス定義の内側ではない場所,つまり トップレベル (top level) での println の呼出しも例外ではない。 与えられた Groovy スクリプト S.groovy のトップレベルに println の呼出しが書かれているとしよう。 このとき groovy.lang.Script の派生クラスとして,ファイル名にちなんで S クラスが暗黙裏に定義される。 そして S のインスタンスがトップレベルの this として与えられて,println が実行される。 2.3 節 の例では,このようにして暗黙裏に定義された hello クラスのインスタンスが println のレシーバとして与えられる。
以上,println メソッドが何者であり,どうしてそのかたちで使われるのかを説明した。
4.3.2 補足: トップレベル
今や長らく正体不明で使ってきた println が明らかになった。 しかし,君は,クラス定義の外側の「トップレベル」についてあまり釈然としていないはずだ。 スクリプトや対話シェルの地の部分が,このトップレベルにあたる。
Java とおなじ基盤に立つ言語なのに,なぜクラスに囲まれない場所で変数やメソッド (関数) を定義できるのか, 定義したとして,Java から見た位置付けはどうなっているのか,今まであまり説明せずに棚上げにしてきた。 できれば説明するほどのことが何もなければよかっただろう。 Groovy によるトップレベルの実現方法は,いくつかのあまり自明でない制限をもたらしている。 しかし,からくりが分かってしまえば,どうということはない。 それを解く鍵は 4.1.1 節で言及し,今また述べた groovy.lang.Script クラスと,暗黙裏に自動生成されるその派生クラスにある。
4.3.1 節 で述べたとおり,与えられたスクリプト S.groovy に対して Groovy は groovy.lang.Script の派生クラスとして,スクリプトのファイル名にちなんだクラス S を暗黙裏に定義する。 対話シェルでも同様の派生クラスを,処理系実装が決めたクラス名で暗黙裏に定義する。
君がトップレベルでメソッドを定義したとき,Groovy はそのメソッドをこの暗黙裏のクラスの非静的メソッドとして解釈する。 これが 4.1.1 節で説明した「関数」の正体である。
println "hello, world"
を含んでいるだけですが,Groovy では,これを概ね次のような Java のクラス定義として暗黙裏に解釈します。
import groovy.lang.Script; import org.codehaus.groovy.runtime.DefaultGroovyMethods; public class hello extends Script { @Override public Object run() { DefaultGroovyMethods.println(this, "hello, wolrd"); } public static void main(String[] args) { Script s = new hello (); s.run(); } }実際,hello.groovy はあたかもクラス hello を定義しているファイルであるかのように import して使うこともできます。
groovy:000> import hello ===> [import hello] groovy:000> h = new hello() ===> hello@ec71f8 groovy:000> h.run() hello, world ===> null
トップレベルで宣言なしにいきなり代入して変数を定義したとき,それは暗黙裏のクラスの非静的 public フィールドとして,事実上,解釈される。 したがって,トップレベルの関数から自由にアクセスできる。 普通のクラスのメソッドからは (素直には) アクセスできない。
それに対し,トップレベルで (型の指定や def を使って) 宣言した変数は,暗黙裏のクラスに自動的に作られる run() メソッドのローカル変数として解釈される。 したがって,同じトップレベルの文 (つまり暗黙裏のクラスの run() メソッドの本体) からはアクセスできるが, トップレベルの関数 (つまり暗黙裏のクラスの run() 以外のメソッド) からはアクセスできない。 もちろん,普通のクラスのメソッドからもアクセスできない。
4.1.1 節 で説明したように,トップレベルで定義した関数はそのままではクラスのメソッドの中から呼び出せません。 つまり,Ruby と異なり,Groovy では,グローバルな関数をユーザが自明な方法で定義することができません。 スクリプトの実際の取り扱いについて説明した今,ようやくこの制限について弁明することができます。
Ruby では,トップレベルで定義した関数は,共通基底クラス Object のインスタンス・メソッド (= 非静的メソッド) として定義されます。 Object クラスそのものが,その場その場でダイナミックに定義されていくわけです。 しかるに Groovy では,Object はあくまで定義済みの Object クラスのままであり,トップレベルの関数は,そのスクリプトの暗黙のクラスの非静的メソッドとして定義されるだけです。 Java との互換性のため,スクリプトにおいて明示的に定義される普通のクラスは,この暗黙のクラスと継承関係などの特別な関係をもちません。 したがって,そこから見れば「関数」は単なる無関係なクラスのメソッドにすぎませんから,明示的なレシーバ無しにはアクセスできないわけです。
もちろん,Groovy の処理系が手間となにがしかの犠牲をいとわなければ,トップレベルの関数を Ruby の関数や Groovy JDK の println などと同じようにグローバルにすることは決して不可能ではなかったでしょう。 例えば,暗黙のスクリプト・クラスをダイナミックに Groovy JDK のメソッド探索メカニズムに組み入れればよいわけです。 しかし,少なくとも今の Groovy はそうしていません。 オブジェクト指向の枠から外れすぎた関数の乱用を防いでいる一面もあります。
トップレベルに対して暗黙裏にクラスが定義され,そのインスタンスが this になるという事実は, this そのものを表示するスクリプトを書いて実験的に確認できる。 下記では .getClass() と .getSuperclass() を使って this のクラスと基底クラスを調べている。 これはそれぞれ .class と .superclass と書いてもよい。
println this c = this.getClass() println c println c.getSuperclass()
$ groovy test1.groovy test1@1f01a29 class test1 class groovy.lang.Script
対話シェルのトップレベルも同様にして調べることができる。
groovy:000> this ===> groovysh_evaluate@1a9883d groovy:000> c = this.getClass() ===> class groovysh_evaluate groovy:000> c.getSuperclass() ===> class groovy.lang.Script
4.3.3 注意: 対話シェルは1行ごとに別々の顔
ここで対話シェルには注意すべき一つの点がある。
対話シェルでは1行ごとに this の値が異なる。 つまり,その Script インスタンスは1行ごとに新しく作られている。
groovy:000> this ===> groovysh_evaluate@c42091 groovy:000> this ===> groovysh_evaluate@56adde
対話シェルのトップレベルで定義したメソッドと,宣言なしに代入して定義した変数は,新しく作られる Script インスタンスに引き継がれる。 しかし,型や def を使って宣言した変数は引き継がれず,その1行限りになる。
したがって,4.1 節 で使った単体のブロックは,実は,対話シェルでは必要ない。 ブロックで囲まなくても,宣言した変数はその1行限りで消える。
groovy:000> def s = 0xFF; println s; println s.getClass()
255
class java.lang.Integer
===> null
groovy:000> s
ERROR groovy.lang.MissingPropertyException:
No such property: s for class: groovysh_evaluate
at groovysh_evaluate.run (groovysh_evaluate:2)
...
4.4 プロパティとフィールドと .@ 演算子
Groovy は Java Beans プロパティを構文的にサポートしている。 クラスにゲッター・メソッド getPoi やセッター・メソッド setPoi が定義されているとき, これをフィールド poi のようにアクセスできる。 例を示す。
groovy:000> class A {
groovy:001> private int i
groovy:002> public int getPoi() { i }
groovy:003> public void setPoi(int x) { i = x; printf "poi is %d%n", i }
groovy:004> }
===> true
groovy:000> a = new A ()
===> A@e5f21e
groovy:000> a.poi = 123
poi is 123
===> 123
groovy:000> a.poi
===> 123
a.poi = 123 は a.setPoi(123) をした後,右辺の値を結果の値として返します。
つまり,L: { a.setPoi(123); 123 } というブロックと実質的に等価です。
一見すると無駄に思えますが, x = a.poi = result のような代入を可能にするために必要です。
さらなる便宜として,第3章で述べたように,フィールドにアクセス制御を指定しないとき, そのフィールドは自動的に Java Beans プロパティとして定義される。 例えば,
class A { int poi }
と書いたとき,実際には下記のように書いたことと等価である。
class A { private int poi; public int getPoi() { poi } public void setPoi(int value) { poi = value } }
ここでさらにゲッター・メソッドまたはセッター・メソッドを陽に定義すれば,デフォルトの振舞を変更できる。 下記は,与えられた値の 10 倍の値をプロパティ値としてセットするように定義した (わざとらしい) 例である。
groovy:000> class A {
groovy:001> int poi
groovy:002> public void setPoi(int x) { poi = x * 10 }
groovy:003> }
===> true
groovy:000> a = new A()
===> A@52ded2
groovy:000> a.poi = 1
===> 1
groovy:000> a.poi
===> 10
groovy:000> a.@poi = 2
===> 2
groovy:000> a.poi
===> 2
ここで a.@poi = 2 の .@
は,メソッドをバイパスして,フィールドに直接アクセスするための演算子である。
4.5 リスト,マップ,レンジ
Groovy はリストとして,第3章 で学んだように式の並びをカギ括弧で囲んで java.util.ArrayList インスタンスを構築できる。 この構築方法は Ruby の Array や Python のリストに似ている。
groovy:000> x = [3, 1, 4, 1, 5, 9] ===> [3, 1, 4, 1, 5, 9] groovy:000> x.getClass() ===> class java.util.ArrayList
リストから生の配列へキャスト演算または as 演算だけで変換できる。
groovy:000> y = (int[]) x ===> [I@e2c656 groovy:000> y.getClass() ===> class [I groovy:000> z = x as int[] ===> [I@1d1cdf7 groovy:000> z.getClass() ===> class [I
変換前後の値は等しい。 オブジェクトとしては新しく作られたものだから,アイデンティティは異なる。
groovy:000> print x [3, 1, 4, 1, 5, 9]===> null groovy:000> print y [3, 1, 4, 1, 5, 9]===> null groovy:000> x == y ===> true groovy:000> x.is y ===> false
リストと生の配列のどちらも Ruby の Array が備えているようなメソッドを使うことができる (5.5 節 参照)。
groovy:000> x.collect { it + 100 }
===> [103, 101, 104, 101, 105, 109]
groovy:000> y.collect { it + 100 }
===> [103, 101, 104, 101, 105, 109]
x = [3, 1, 4, 1, 5, 9]
x.collect {|e| e + 100 }
よく似ていますが,Groovy では e を宣言せずに暗黙の it を使っています。
e を宣言する場合,Groovy ではこうします (5.1 節 参照)。
x.collect { e -> e + 100 }
Ruby の Array に適用できるような演算子も使える。下記では 2 を末尾に追加している。
groovy:000> x << 2 ===> [3, 1, 4, 1, 5, 9, 2] groovy:000> x ===> [3, 1, 4, 1, 5, 9, 2]
実際には x << 2 は x.leftShift(2) の構文上の糖衣である。
ただし,生の配列は Java と同じくあくまで固定長だから,末尾に要素を追加することはできない。
エラーメッセージから,leftShift という生のメソッド名を隠している糖衣のほころびを見ることができる。
groovy:000> y << 2
ERROR groovy.lang.MissingMethodException:
No signature of method: [I.leftShift() is applicable for argument types: (java.l
ang.Integer) values: [2]
at groovysh_evaluate.run (groovysh_evaluate:2)
...
Groovy のマップの構文は Ruby の Hash とも Python の辞書とも異なる。 Groovy では,カギ括弧で囲みカンマで区切った並びの各要素として,コロンで組みにしたキーと値のペアを与える。 こうして構築される java.util.Map インスタンスは正確にはその実装クラス java.util.LinkedHashMap のインスタンスである。
groovy:000> apples = ["hu": "alma", "fi": "omena", "tr": "elma"]
===> {hu=alma, fi=omena, tr=elma}
groovy:000> apples.getClass()
===> class java.util.LinkedHashMap
ここでマップを構築したときの結果の値を見ると,並びがカギ括弧ではなく波括弧 { }
で囲まれていること,キーと値が等号で組みにされていることに気付く。
これは Java による実装クラス java.util.LinkedHashMap の toString()
の戻り値がそのまま表示されているからである。
Groovy 自身のマップの構文ではないことに注意しよう。
またこのとき,マップの各要素が与えたとおりの順序で表示されているのは決して偶然ではない。 java.util.LinkedHashMap はマップへのキーの追加順序を保存する。
キーに対する値をセットしたり取得するときは,添字演算子を使うことができる。
groovy:000> apples["eo"] = "pomo" ===> pomo groovy:000> println apples [hu:alma, fi:omena, tr:elma, eo:pomo] ===> null
Groovy が Ruby に直接ヒントを得たデータ構造としてレンジ (range, 範囲) がある。 与えられた範囲の値を順に格納するように構成したリストと似ているが,始端と終端以外の値をじかに持たずに要求に応じて作って返すだけだから, リストに比べて空間的な効率が極めて良い。
終端を含むときの構築方法は Ruby の Range と同じである。
groovy:000> ii = 1..5
===> 1..5
groovy:000> ii.getClass()
===> class groovy.lang.IntRange
groovy:000> for (i in ii) printf("%d, ", i)
1, 2, 3, 4, 5, ===> null
Ruby と異なり,終端を含まないときは 構築演算子として ..< を使う。
groovy:000> jj = 1 ..< 5 ===> 1..4
整数に限らず,next() と previous() のメソッドを実装し, java.lang.Comparable インタフェースを実装しているならば,何であれレンジの始端と終端にできる。
groovy:000> "s".previous() ===> r groovy:000> "s".next() ===> t groovy:000> "s" instanceof Comparable ===> true groovy:000> cc = "a".."z" ===> a..z
始端と終端は from と to で得られる。 これは Comparable getFrom() と Comparable getTo() で実装されるプロパティである。
groovy:000> cc.from ===> a groovy:000> cc.to ===> z
レンジは java.util.Collection であり,さらに言えば java.util.List である。
groovy:000> cc instanceof java.util.Collection ===> true groovy:000> cc instanceof java.util.List ===> true groovy:000> cc.getClass() ===> class groovy.lang.ObjectRange
したがって,Groovy の他の Collection インスタンスと同じく様々な操作が可能である (5.5 節 参照)。 そうしようと思えば,最小値と最大値を得るための (レンジにとっては愚かな) 手段として min() と max() を使うこともできる。
groovy:000> cc.min()
===> a
groovy:000> cc.max()
===> z
groovy:000> cc.each { printf("%s-", it) }; println()
a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-
===> null
cc = "a".."z"
cc.each {|e| printf("%s-", e)}; puts
この節では二,三の例で,等価な Ruby コードを示すこと以外,特に説明せずにクロージャを使ってきた。 実行結果からそのおおよその動作と意味はつかめていると思う。 最後の例では each が for 文と同じ種類の制御文に見えているかもしれない。 事実,それは制御変数 it にレンジの各要素を代入して { } 内にある printf メソッドの呼出しを繰り返している。
しかし,これは言語に作り付けの制御文ではなく,自由に定義して組立てることのできる単なるメソッドの呼出し式である。 ただ,そのメソッドが,コードのかたまりである「クロージャ」を引数にとることで,制御文と同じような振舞を実現しているにすぎない。 逆にいえば,クロージャを使えば,君独自の新しい制御文 (であるように見え,そのように機能するもの) をメソッドとして作ることができる。
クロージャを引数にとるメソッドは,この数十年間,関数型言語の世界で高階関数 (higher-order function) として広く知られ活用されてきたものと基本的には同じである。 J. Hughes が 「なぜ関数プログラミングは重要か」 [sampou.org] でいみじくも説明しているように,高階関数は,問題を部分に分解し組立てるための有用な道具になる。 それなしでは考えもしなかったかたちでコードを再利用でき,生産性向上に役立つライブラリを構築できる。 その効用は Miranda や Haskell のような厳密な意味での関数型言語に限らず, Lisp や ML のような純粋ではない関数型言語,さらには Smalltalk-80 や Ruby にも及び,現在さらに幅広く普及しようとしている。
次章では,いよいよ,このクロージャについて学ぼう。
オブジェクト指向の元祖のように言われている 1980 年ごろの Smalltalk-80 は,当時すでにブロック (block) という用語でクロージャに相当する言語要素を導入し,(それ自身はそうは呼称していなかったものの) 高階関数プログラミングをその基盤としていました。 条件分岐の if 文でさえ,Boolean クラスに対するブロック付きメソッドとして与えられていました。
(self respondsTo: operator) ifFalse: [self error: 'operation not understood']その後の C++ の隆盛により,いったんはほとんど忘れ去られましたが, もともとその最初の日々からオブジェクト指向はクロージャとともにありました。 その伝統は部分的に Ruby に継承され,さらに Groovy が Ruby から継いでいます。 クロージャを活用することは,ある意味,オブジェクト指向言語が単に長く忘れていた力を復活させることです。
参考: A. Goldberg & D. Robson: "Smalltalk-80: The Language and its Implementation", Addison-Wesley, 1983, ISBN 0-201-11371-6
5. クロージャ
クロージャ (closure, 関数閉包) とは,一般に,環境 (つまり,自由変数からその変数値への写像) を伴って引き渡される関数である。
特に Groovy では groovy.lang.Closure クラスのインスタンスを意味する。 その実態は一般的なクロージャの定義と矛盾しておらず,妥当な命名である。 Groovy は Closure のインスタンスを構築する特別な構文を下記のように用意している。
{ 仮引数並び -> 宣言や文の並び }
ここで「仮引数並び」はメソッド定義での仮引数並びと同様である。 「宣言や文の並び」はメソッド定義でのメソッド本体と同様である。
Closure インスタンスに実引数を与えて,メソッドのように呼び出すことができる。 メソッド呼出しと同じく,この呼出しが最外の式であって,実引数が1個以上あれば,実引数を取り囲む丸括弧を省略してよい。
groovy:000> c = { int x, int y -> int z = x + y; return z * 2 }
===> groovysh_evaluate$_run_closure1@a2bf0f
groovy:000> c(2, 3)
===> 10
groovy:000> c 2, 3
===> 10
groovy:000> c instanceof Closure
===> true
groovy:000> c.class.superclass
===> class groovy.lang.Closure
この例から分かるように,各クロージャの Java VM 上の実装クラスは, groovy.lang.Closure の派生クラスとして Groovy によって自動的に定義される。 これは実装詳細であり,普段は Closure インスタンスとしてだけ意識すればよい。
5.1 省略記法
1引数のクロージャで仮引数の型を指定しないならば,仮引数の並びと -> を省略してよい。 仮引数は it として参照される。
groovy:000> c = { it * 2 }
===> groovysh_evaluate$_run_closure1@b1164d
groovy:000> c 3
===> 6
0引数のクロージャならば,-> を省略してよい。
groovy:000> c = { println "Hi ho!" }
===> groovysh_evaluate$_run_closure1@500082
groovy:000> c()
Hi ho!
===> null
このように省略したとき,クロージャとブロックを,それ自体の構文から区別することはできない。 ただし,ほとんどの場合,これは問題にならない。 式の一部として出現した場合は,クロージャと判定されるからだ。 この例では代入の右辺として出現しているから,クロージャと判定される。
しかし,文として出現した場合は,どちらなのか判定できないため,構文解析のエラーが発生する。 その場合,-> を省略しないで書くことによって,クロージャであると明示できる (一方,第3章 では,ブロックであると明示するために適当なラベルを接頭した)。
groovy:000> def say() { { -> "Nukata" } }
===> true
groovy:000> say()
===> groovysh_evaluate$_say_closure1@62974e
groovy:000> say()()
===> Nukata
5.2 静的スコープと this
クロージャとは単なる引数付きのコードの並びではない。 もしそうだったならば,クロージャを引数にとるメソッドを (普通の制御文であるかのように) なにげなく自然に使うことができなくなる。 そうしないための鍵は自由変数の取り扱いにある。
クロージャの自由変数 (free variable) とは, クロージャの仮引数でも,クロージャ内部で宣言された変数でもない,クロージャの外部からの変数である。
{ x, y -> int z = a * (x + y); return z }
では,a はこのクロージャの自由変数, x と y は仮引数 (束縛変数, bound variable),
z はクロージャ内部で宣言されたローカル変数 (束縛変数) です。
クロージャは自由変数を,クロージャが作られた場所から見える変数と解釈して参照する。 たとえクロージャがメソッドの戻り値として返され,あるいは引数として渡され, どこか見知らぬ別のクラスのメソッドの中に持っていかれたとしても,クロージャが呼び出されたとき, それが参照する自由変数は,必ず,クロージャの生まれ故郷の風景にある変数である。 クロージャは,遠くにあってその風景を見るよすがとして,常に環境 (environment) を随伴させている。
オブジェクト指向言語では,この議論はとりわけクロージャ内の this が何を指すかという問題と関係する。 結論は明快である。 this をクロージャにとって自由変数であるとしたとき, クロージャ内の this は,クロージャがどこで実行されようとも,クロージャが作られた産土の this を参照する。
下記の例は,クラス A のメソッドの中で作られたクロージャが,どこへ持っていかれ,どこで実行されようとも, あくまでクラス A のインスタンスを this として参照することを示している。
groovy:000> class A {
groovy:001> def c() { return {-> println this} }
groovy:002> }
===> true
groovy:000> class B {
groovy:001> def f(c) { println this; c() }
groovy:002> }
===> true
groovy:000> a = new A()
===> A@969e65
groovy:000> c = a.c()
===> A$_c_closure1@8b30ae
groovy:000> c()
A@969e65
===> null
groovy:000> b = new B()
===> B@9a2097
groovy:000> b.f(c)
B@9a2097
A@969e65
===> null
これは 1970 年に提起された FUNARG 問題 [wikipedia.org] を今の言葉で表現したものです。 関数を変数に代入したり,戻り値としたりするプログラミング言語は,歴史的には Lisp が最初期のものでしたが, 長い間このような,今の目で見れば気違いじみたことが平然と行われていました。時代にさきがけて 1960 年に Algol 60 [masswerk.at] が静的スコープを採った意義は当時まだ広く理解されていませんでした。
静的スコープ (static scope, 字句的スコープ lexical scope とも) とは,変数のスコープつまり有効範囲の種類の一つであり,有効範囲がプログラムの静的な字面から決まるものをいう。 クロージャは,その郷土愛にあふれる振舞によって静的スコープを具現化している。 静的スコープでは,プログラムに書かれた変数名が,どの変数のことを意味するのか,プログラムの実行前に分かる。 この長所によって,Groovy に限らず,現代のプログラミング言語のほとんどが静的スコープを採用している。
当初,静的スコープを採用しなかった言語も,その多くは進化の途上でそれを採用しています。 近代的な Lisp の歴史は,Algol 60 にならって静的スコープを全面的に採用した Scheme,および概ね同じ設計者により旧来の Lisp との折衷で作られた Common Lisp から始まります (標準 Pascal によるモダンな Lisp の小さな実装 §4 および PLY で作る Algol 60 処理系 §2 も見てください)。
今日,ラムダ式とクロージャはほとんど同義語のように扱われています。 しかし,Algol の血が混じる前の Lisp では両者は別物でした。当時,
'(lambda (x) (f x a))を評価した結果はラムダ式であってもクロージャではありませんでした。 自由変数 a はラムダ式が評価された場所から見える a と解釈されました。 クロージャにするには,
(function (lambda (x) (f x a)))のようにする必要がありましたが,こうすることは効率と引き換えでした (ただし,純血種の Lisp の末裔である Emacs Lisp [gnu.org] ではこうしてもクロージャを作りません。 エディタの実装言語として割り切っているためか,単にバイトコンパイルの対象として印づけるだけです)。
Python は当初は独特なスコープ則を採っていましたが,Python 2.1 で静的スコープを採用しました (PEP 227 [python.org])。 これはプログラムの意味論の根幹にかかわる非互換的・抜本的な改良でした。 これ以前の Python の文献は独特なスコープ則の説明に,初心者はその理解に苦労していました。 それが突然すべて解決したのです。 これと比べると,互換性を犠牲にした大改訂版のように言われている Python 3.0 は確かにちくちくと非互換性がありますが, 実際には,いつもの改訂だったらとても理由を正当化できない どちらでも大差ないような変更が大半です。 本稿著者による公開プログラムでは,例えば Prelude.py の outchar 関数に Python の静的スコープの利用例があります。
- Scheme:
- (define (foo n) (lambda (i) (set! n (+ n i)) n))
- Arc:
- (def foo (n) [++ n _])
- Groovy:
- def foo(n) {{i -> n += i}}
groovy:000> def foo(n) {{i -> n += i}}
===> true
groovy:000> f = foo(3)
===> groovysh_evaluate$_foo_closure1@f91dd6
groovy:000> f(1)
===> 4
groovy:000> f(5)
===> 9
groovy:000> g = foo(100)
===> groovysh_evaluate$_foo_closure1@de5d00
groovy:000> g(2)
===> 102
groovy:000> g(3)
===> 105
groovy:000> f(2)
===> 11
Python ファンへ:
同エッセイでは Python が否定的に書かれています。
滑った感がある Python 3.* ですが,実のあるささやかな新機能の一つとして PEP 227 の静的スコープを補完する
PEP 3104 [python.org]
の nonlocal 宣言があります。
これを利用すれば同エッセイで批判されている Python プログラムを改良できます。
どう書き直せばよいか?,いまだ残る課題は何か? は Python ファンの読者への課題とします。
5.2.1 補足: delegate プロパティ
前節ではクロージャのスコープについての基本的な原則を述べたが,実はまだこれは真実のすべてではない。 前節の議論と矛盾しない範囲で Groovy は,その出生地からは見えないメソッドをクロージャから利用できるように, delegate プロパティを使ってスコープを拡張している。
delegate プロパティは Groovy のどの Closure インスタンスにもある。 このプロパティはクロージャが作られた時点では,その this の値と同じに初期化されるが,後から自由に変更できる。 そして,もしもクロージャ内で変数やメソッドの名前を解決できないときは,this のかわりに delegate の値を起点として名前の探索が再試行される。
下記のコードでこのことを説明しよう。
class A { // static foo(x) { x - 10 } static Closure c = { foo(bar) } } println A.c.delegate // => class A class B { def foo(x) { x + 1 } def bar = 100 def c = A.c } b = new B () b.c.delegate = b println b.c() // => 101
まず,クラス A に,静的フィールドとしてクロージャ c を設ける。 このクロージャの delegate プロパティの初期値は,その this と同じく class A である。 クロージャ内では bar を引数として foo を呼び出すが,foo と bar のどちらもクロージャが作られたクラス A からは見えないことに注意しよう。
次に,クラス B にフィールド c を設ける。初期値は今しがたのクロージャ A.cとする。
それから,クラス B のインスタンス b を作り,b.c.delegate = b として delegate プロパティの値を b とする。
最後に b.c() を呼び出すと,クロージャ内の foo(bar) が実行される。 しかし,foo という名前はそのクロージャの出生地であるクラス A からは見つからない。そこで delegate プロパティの値であるインスタンス b を起点として探索し直すと,クラス B のメソッド foo が見つかる。 bar についても同様に探索し直してクラス B の bar が見つかる。 こうして 100 + 1 が実行されて 101 が得られる。
class A 101
もしもこのとき,クラス A で今はコメントアウトされている foo メソッドの定義を活かしたとすると,当然そちらが優先して使われることになる。 したがって,この場合は 100 - 10 が実行されて 90 が得られる。
つまり,静的スコープの基本原則に従い,可能な限りそのクロージャの出生地 (で決まる this) が優先される。 それで解決されない名前についてだけ,delegate による動的スコープが実施される。
5.3 クロージャ実引数の特別規則
前節の例で示したように,クロージャは,普通の式と変わるところなく実引数としてメソッドに渡すことができる。 機能としてはこれで十分だが,さらに Groovy は,人間が利用しやすいように,ある特別な構文規則を導入している。 メソッド呼出しの丸括弧の後に波括弧をおいてクロージャを書くことができる,という規則である。
下記に例を示す。
groovy:000> def f(x, y, z) { println x; println y; println z }
===> true
groovy:000> f(1, 2, 3)
1
2
3
===> null
groovy:000> f(1, 2, {a -> a + 2})
1
2
groovysh_evaluate$_run_closure1@dd95f6
===> null
groovy:000> f(1, 2) { a -> a + 2 }
1
2
groovysh_evaluate$_run_closure1@87e18f
===> null
最後の呼出し f(1, 2) { a -> a + 2 } に注目しよう。
これは意味としては f(1, 2, {a -> a + 2}) と変わらない。
しかし,人の目には,あたかも while (…) { … } や
synchronized (…) { … } と同類の制御構造に見える。
つまり,クロージャを最後の引数とするメソッドを定義することで,見かけ上,新しい制御構造を定義できる。
自由変数を,クロージャが書かれた場所から見える変数として参照する,という静的スコープのルールは, このようなクロージャの使い方とマッチしている。
0引数または1引数のクロージャのとき,省略記法を使えば,ぎこちない「->」を消すことができる。
groovy:000> f (1, 2) { it + 2 }
def my_each(list) { return { closure -> list.each(closure) } } my_each([2, 7, 1, 8]) ({ println it + 100 })
5.4 Ruby との違い
クロージャを末尾の引数にとる Groovy のメソッドは Ruby の ブロック付きメソッド と多くの共通点をもつ。 しかし,Groovy は決して Ruby の単純な引き写しではない。
Ruby のブロック付きメソッドには,暗黙のうちに渡される (Ruby の用語法でいう) ブロックと,それを呼び出す yield,優先順位の異なる2種類の文括弧など,歴史的な事情を引きずる数々の言語要素が関係している。 一方,Groovy は,そのほとんどを捨て去り,一般概念に沿ったクロージャを導入し,使い勝手のために ごく少数の簡単な省略記法や特別規則を採用することで同等の効果を達成している。
言い換えれば,ブロックがファーストクラスのオブジェクトだった Smalltalk の単純さへ戻っている,ともいえます。 しかし,Smalltalk は if-then-else の構造さえもブロック付きメソッドで用意する,今となってはなじみのない言語です。 よく知られている Java と Ruby から自然な類推ですぐに使える外形になるように留めたのが,今の Groovy だと言ってよいかもしれません。
Groovy が一般概念に沿ったクロージャを採用したことの帰結として,Ruby との間に特筆すべき重要な違いが一つある。
Ruby のブロックの中で return を実行したときは,ブロックだけでなくそれを取り囲むメソッドが終了する。 しかし,Groovy のクロージャの中で return を実行したときは,Ruby の next でブロックが終了するのと同じように,そのクロージャの呼出しだけが終了する。 つまり,おおまかに言って,次の等式が成り立つ。
Groovy の return = Ruby の next
例を示そう。
Groovy は Ruby にならって数に対し times メソッドを追加している。 これは,その数だけの回数,クロージャを呼び出すメソッドである。 このクロージャの中で return したとき,次の繰り返しへと進む。 これは Ruby の next と正確に同じ動作である。 ループを強制終了するには,例外を throw して times メソッドから脱出する必要がある。
groovy:000> 5.times { print "No."; println it }
No.0
No.1
No.2
No.3
No.4
===> null
groovy:000> 5.times { print "No."; if (it == 2) return; println it }
No.0
No.1
No.No.3
No.4
===> null
しかし,純粋にクロージャとして考えれば,return で終了する Groovy の振舞の方が素直で理解しやすい。 Ruby のブロックを途中で終了するにはどうすればよいかは,多くの Ruby 初心者がつまずく問題である。
5.5 Ruby 由来のイテレータ
前節の times メソッドのほか,Groovy は Ruby に由来するイテレータをいくつか追加している。
groovy:000> 1.upto(3) { printf "%d ", it }
1 2 3 ===> null
groovy:000> 3.downto(1) { printf "%d ", it }
3 2 1 ===> null
groovy:000> 1.step(10, 2) { printf "%d ", it }
1 3 5 7 9 ===> null
groovy:000> [3, 1, 9].each { printf "%d ", it }
3 1 9 ===> [3, 1, 9]
とりわけ,Ruby の Enumerable モジュールのメソッドと同様のメソッドをいくつか Groovy JDK として用意している。
groovy:000> x = [3, 1, 4, 1, 5, 9]
===> [3, 1, 4, 1, 5, 9]
groovy:000> x.collect { it + 0.5 }
===> [3.5, 1.5, 4.5, 1.5, 5.5, 9.5]
groovy:000> x.findAll { it > 2 }
===> [3, 4, 5, 9]
groovy:000> x.inject(1) { x, y -> x * y }
===> 540
groovy:000> x.sort { x, y -> y <=> x }
===> [9, 5, 4, 3, 1, 1]
groovy:000> x.min()
===> 1
groovy:000> x.max()
===> 9
この部分だけ見るならば,Groovy は Ruby の1方言であるとしか思えない。
x.findAll { it > 2 }
については少し説明が必要でしょう。
Ruby では find_all というメソッド名です。
x.find_all {|e| e > 2}
のように書きます。Groovy は Ruby に倣いつつも基本は Java 言語ですから,Java
の流儀に従い,これを findAll としています。
x select: [ :each | each > 2 ]のように書きます。予想されるとおり,Ruby は find_all の同義語として select も用意しています。
x.select {|e| e > 2}
しかし,Groovy はこちらは用意していません。
Ruby のコレクション機能は Smalltalk から多くを継いでいますが,Groovy のそれは
Smalltalk 由来というよりは Ruby 由来というわけです。
ただし,Ruby の Enumerable モジュールに直接相当するものは Groovy にはない。 Groovy は次のようにコレクション用の Groovy JDK メソッドを提供している。
-
java.lang.Object へ
Object each(Closure),List collect(Closure),Collection findAll(Closure),Object inject(Object initialValue, Closure)などの基礎的なコレクション用メソッドを追加 -
java.util.Collection へさらに
List sort(Comparator),Object min(),Object max()などの付加的なコレクション用メソッドを追加
コレクション用のメソッドに対して,整数などのオブジェクトは,要素数1のコレクション (つまり,数学用語としての singleton, 単集合) として振舞う。 その一方,おそらく君が予想しているとおり,文字列や生の配列は,添字演算子で取得できるような要素,つまり長さ 1 の文字列や各配列要素を順にクロージャ引数に与える。
groovy:000> 1234.each { println it + 56 }
1290
===> 1234
groovy:000> "1234".each { println it + 56 }
156
256
356
456
===> 1234
6. おわりに
完全な説明には遠いが,最初の基礎として十分な程度には Groovy の言語仕様を君に伝えた。 Groovy には,広く普及した Java の知識と経験をいかすことができ,既存の Java システムに組み込むことができる即戦力の道具としての側面と,草創期からの Ruby にあった複雑さを切り捨て,その良いアイディアから新しく構成した軽量言語としての側面がある。 名前のセンスに疑問があるというだけで敬遠するには惜しいことが,今の君には理解できると思う。
7. 参考文献
- Groovy - Home: http://groovy.codehaus.org
- D. König et al. (著),関谷 et al. (訳):「Groovy イン・アクション」, 毎日コミュニケーションズ, 2008, ISBN 978-4-8399-2727-1
- 関谷 et al.:「プログラミング Groovy」, 技術評論社, 2011, ISBN 978-4-7741-4727-7
文献 1. は Groovy の公式ページである。 実際の Groovy プログラミングでは,とりわけ 4.3 節 で紹介した Groovy JDK API Specification [codehaus.org] が基礎的な資料として重要である。
文献 2. は,日本語で読むことができる Groovy のまとまった資料である。 ていねいな記述と翻訳は,本来ならば良書と評価すべき質である。 しかし,この本には索引が貧弱で,ほとんど役立たないという欠陥がある。 実際のプログラミングで参照するために,読者は,約 600 ページの大部の本のどこに何が書かれているか記憶する必要がある。 検索可能な電子化文書として発行されるか,あるいは索引が増補されることが望まれる。
文献 3. は,新しく日本語で書き下ろされた,よりコンパクトな本である。 プログラム・コードの行間が空きすぎて読みにくいきらいがあり,ページ数の制約からかもう一歩説明が足りないところがあるが,広範な Groovy のトピックを分かりやすく手引きしている。 近年の Groovy の様々な側面を知り,Groovy を効果的に活用するための必読書といってよい。
付録 1. Emacs 編集モード
テキスト・エディタ Emacs で Groovy のコードを編集するためのモードを実現する groovy-mode.el は下記から入手できる。
Groovy Emacs Mode [launchpad.net]
2012年 2月 3日現在の現行版は emacs-groovy-mode_2011-06-29.tgz である。 展開するといくつかファイルがあるが,さしあたり必要なのは groovy-mode.el である。 groovy-mode.el を Emacs Lisp の load-path が通っている場所 (例えば,/usr/share/emacs/site-lisp や ~/Applications/Emacs.app/Contents/Resources/site-lisp など) に置き,~/.emacs ファイル (または ~/.emacs.d/init.el など相当するファイル) に下記を追加する。
(autoload 'groovy-mode "groovy-mode" "Groovy editing mode." t)
(add-to-list 'auto-mode-alist '("\\.groovy$" . groovy-mode))
(add-to-list 'interpreter-mode-alist '("groovy" . groovy-mode))
1行目は Emacs Lisp 関数として groovy-mode が呼び出されたとき, "groovy-mode" という名前に拡張子をつけてファイルを読み込むようにする。
2行目は .groovy という拡張子のファイルを編集するとき, groovy-mode を呼び出して編集モードを切り替えるようにする。
3行目は #! ではじまるスクリプトを編集するとき,
指定されているインタープリタ名が groovy ならば (例: #!/usr/bin/env groovy),
groovy-mode を呼び出して編集モードを切り替えるようにする。
もしも Emacs に閉じ括弧などの自動挿入をさせたいならば, groovy-mode.el と同じ場所に groovy-electric.el を置き, ~/.emacs ファイルまたは相当するファイルに下記を追加する。
(add-hook 'groovy-mode-hook (lambda ()
(require 'groovy-electric)
(groovy-electric-mode)))
キーワードなどに対する色や書体の指定は,他の編集モードと共通である。 下記に ~/.emacs ファイルでのカスタマイズの一例を示す。
(if window-system
(progn
(set-face-foreground font-lock-variable-name-face "black")
(set-face-foreground font-lock-keyword-face "NavyBlue")
(set-face-foreground font-lock-function-name-face "MediumBlue")
(set-face-foreground font-lock-type-face "DeepSkyBlue4")
(set-face-foreground font-lock-string-face "DarkGreen")
(set-face-foreground font-lock-builtin-face "black")
(make-face-italic font-lock-variable-name-face)
(make-face-italic font-lock-comment-face)
(make-face-bold font-lock-keyword-face)
(make-face-bold font-lock-builtin-face))
(set-face-foreground font-lock-keyword-face "blue")
(set-face-foreground font-lock-function-name-face "black")
(set-face-foreground font-lock-variable-name-face "black")
(make-face-bold font-lock-variable-name-face)
(make-face-bold font-lock-type-face)
(set-face-foreground font-lock-type-face "black")
(set-face-foreground font-lock-comment-face "red"))
付録 2. NetBeans プラグイン
統合開発環境 NetBeans [netbeans.org] は Groovy を標準でサポートしている。 ダウンロードバンドルによっては NetBeans 本体とともに Groovy 用のプラグイン「Groovy および Grails」をインストールできるが,そうでない場合も後からネットワーク経由で追加できる (「ツール」 → 「プラグイン」を選ぶ)。
プラグインをインストールした NetBeans 上で Groovy をとりあえず試すには「ファイル」→「新規プロジェクト」からカテゴリ「サンプル」 の「Groovy」にある「Groovy-Java のデモ」を選ぶ。 サンプル・プロジェクト「GroovyJavaDemo」が作られる。
ソース・ファイルは DisplayDialog.java と DisplayProvider.groovy の二つである。 後者のコードは 4 行しかないが,Groovy でアクセス制御を指定せずに定義したフィールドが Java から普通の Java Beans プロパティとして利用できる実例となっている (4.4 節)。
Groovy に関しては 2009年の夏ごろから NetBeans はあまり変わっていない (当時の Windows 上の NetBeans 6.7)。 現行の NetBeans 7.1 に同梱されている Groovy 1.6.4 は 2009年の 7月から 8月の変わり目にリリースされた。 2.2 節 でインストールした新しい Groovy 1.8.5 をこのサンプル・プロジェクトで使うには,例えば次のようにすればよい。
- 「プロジェクト」タブの「ライブラリ」を右クリックする。 「JAR/フォルダを追加」で,Groovy 1.8.5 のフォルダの embeddable にある groovy-all-1.8.5.jar を選択する (君はこの JAR ファイルを 2.4 節 で使った)。 「プロジェクト」 タブの「ライブラリ」に groovy-all-1.8.5.jar が追加される。
- 「プロジェクト」タブの「ライブラリ」から Groovy 1.6.4 - groovy-all.jar を削除する。
- もし「ファイル」タブに dist が生成されており,その lib の下に groovy-all.jar が残存しているならば,「実行」メニューの「主プロジェクトを削除して構築」を実行する。 残存している groovy-all.jar が自動的に削除される。





