実装から学ぶ型クラス…Groovyで型クラスを実現することを例にして
これは2014年のG*アドベントカレンダーの第23日目の記事のつもりでしたが、12時すぎてしまいましたorz。
HaskellやScalaやRustには型クラスという言語機能があり、個人的感想として、理解が難しかったものの一つです。いわく、インターフェースのようなもの。いわく、オープンクラスのようなもの、など。
わからなければ作ってみるのが一番です。なのでGroovyで型クラスを実装してみました。
ソースはこちら。
ただし実用的なものではなく、学習用です。また実装したのは概念のコア部分のみで、言語によって詳細は異なることに注意ください。
型クラスとは何か
型クラスとは、多相型・ジェネリクス型の型引数(仮型引数)に対して、「ある型に対して可能な操作の集合」を、制約として与え、またそれらの操作が可能であるという保証を、「クラスの継承関係」とは無縁の方法で与えるものです。
別の言い方で言うと、「クラスにデータを従属させる」「メソッドとデータを一体として管理する」「代入可能性を、クラス継承関係で与える」というようなOOPの考えかたの侵襲を拒否し、クラス継承を使用せずに、型引数を制約する方法です。
型クラスが無い場合、たとえばGroovyにおいてメソッドジェネリクスを使用した以下のコード
static<T> void runSomething(T a) { a.run() }
を、正しいコードとして静的型チェックを通すためには、
static<T implements Runnable> void runSomething(T a) { a.run() }
のように「TはRunnableを継承している」と指定します。Runnableという操作の集合による制約を、引数aの型Tに対して行ない、それによってaに対して特定の操作ができるという保証もしています。
これはこれで良いのですが、型クラスが必要になる動機は、ジェネリクス型変数の制約はしたいのだが、このようなクラス継承の使用を避けたいということです。
クラス継承による制約の何が嫌なのか
なぜ避けたいかといえば、
- クラス継承がないから(Haskellの場合)
- 多相型変数Tを「<T extends XXX>」のように制限するためには、TがXXXのサブクラスか実装クラスとして宣言されている必要がある。もし、渡したい引数のクラスがもともとそのように定義されていない場合、
- Tとして渡すクラスのソースを変更してimplements XXXを追加したりメソッドを追加する。しかし使用用途によって都度クラス定義が変化させたり、追記していかなければならないのが嫌
- Tとして渡すクラスを継承し、XXXを実装するクラスを定義して、新規インスタンス作ってメンバをコピーして渡す。でもコピーでは意味が変わってしまうので嫌(ラッパーと同程度、もしくはそれ以上に嫌)。Scalaのnew with Traitも同様。
- Tとして渡すクラスから継承させることがなんらかの理由(プリミティブ型であったり、finalクラスである場合)で不可能だったり、嫌であるなら、以下の何れかの方法でラッパークラスを使う:
- XXXを継承するクラスのメンバーとしてTを定義する。
- Groovyのトレイトの動的proxy。
- →でもラッパーを導入するとアイデンティティが破壊されるのでいやだ。オーバーヘッドが嫌だ。
- 関数型プログラミングスタイルの一流派としてあり得る方針としての「データと関数の分離」に反するので嫌なのだ
- 「継承不要」「ラッパー不要」というのはパラメトリック多相*1で普通にできていた話なのに、制約を付けようとした時点で必要になるというのは嫌だ。なるべく同じようにやりたい
- なんでもクラスにするという基本方針がそもそもの間違いで、どう覆い隠そうとしてもシンプルさが損なわれる(本質的批判)
- オブジェクトが保持するvtableへのリファレンスなどのメモリコスト、vtableを介在したメソッド呼び出しの実行時コストを避けたい*2。また、データ型のメモリイメージ表現のC言語などとの互換性を常に保持したい*3。
- 仮想関数による多態性イラネ。ていうより、原語では同じ単語で多相性とかぶってるかぶってる。
- 操作のレシーバーという概念は非対称でいやだ。レシーバーと他の引数、戻り値の型に非対称性があり、数学的概念の記述などでの一般性を阻害する
など。まるでイヤイヤ期の幼児のような、嫌だ嫌だ、の連発です。まあ嫌なのだからしょうがない。私もこう列挙されると嫌な気になってきます。
型クラスの仕組みの4段階
型クラスの仕組みを以下の4段階で考え、Groovyで実装していきます。
- (1)型クラスを定義する
- (2)型クラスのインスタンス化
- (3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
- (4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
それぞれに付いてモノイド型クラスのコードを例にして解説します。
ちなみに、今回サンプルとしては以下の型クラスを作っています。
型クラス interface Monoid<T> { interface Functor<F> { interface Applicative<A> extends Functor<A> { interface Monad<M> extends Applicative<M> { interface Show<T> { trait Eq<T> { // One of eq or neq, or both should be overriden. trait Ord<T> extends Eq<T> { インスタンス class OptionalMonoid<T> implements Monoid<Optional<T>> { class ListFunctor implements Functor<List> { class OptionalFunctor implements Functor<Optional> { class ListApplicative extends ListFunctor implements Applicative<List> { class ListMonad extends ListApplicative implements Monad<List> { class IntShow implements Show<Integer> { class StringShow implements Show<String> { class ListShow<T> implements Show<List<T>> { class IntEq implements Eq<Integer> { class IntOrd implements Ord<Integer> {
(1)型クラスを定義する
モノイド型クラスを定義します。型引数に適用する制約を定義するものです。
@TypeChecked interface Monoid<T> { def T mappend(T t1, T t2); def T mempty(); }
このようにします。Tではなくdef Tなのは、なぜかそうしないとコンパイルが通らないから。
見るとわかるように普通のインターフェースです。ここでインターフェースにする理由は、継承が使えるからです。例えば、Monad extends Functorのように型クラスを継承することができます。
これは先程の「クラス継承を不要としたい」に反するように思えますが、実際にはthisからのオフセットによるインスタンス変数の参照やアクセス、thisを介した仮想テーブル(vtable)とその呼び出しの機構は完全に不要で、メソッド置き場として使っているにすぎません。なので、ここでクラス継承を使用するのは便宜的なもので、本来なら、staticメソッドにして、staticメソッドを継承階層をたどって探索してくれる仕組みをAST変換で作り込めば良いところです*4が、手をぬきます(Scalaではobjectにするところ)。Monoidはデータメンバを持たない(インターフェースなので持てませんが、classで実装したとしても持たせない)ことに注意ください。
ここでやっていることをまとめると、
(2)型クラスのインスタンス化
型クラスのインスタンス化とは、制約を与えたいデータ型が、ある型クラス制約を満たす、という宣言であり、満たせるようにするための操作を補充します。
以下では、String型がMonoid型クラスの制約を満たすように、「StringのMonoid型クラスのインスタンス」を作成します。
def instance_Monoid_java$lang$String_ = new Monoid<String>() { @Override String mappend(String i1, String i2) { i1+i2 } @Override String mempty() { "" } }
型クラスのインスタンスを保持する変数名を、「instance_Monoid_java$lang$String_ 」という決められた規約に従うものにしておきます。これはスコープの探索をサボるためです。変数名がインスタンスを決め、同名のインスタンスがあれば、内側の物が勝ちます*5。この変数名の変数をstatic importすることも可能であることも期待します。
ここまでは、何の変哲もない標準のGroovyの機能で実現することです。
(3)型クラスで制約した多相型引数を含む型の引数をとる関数を定義する
さて、準備した型クラスMonoidの制約を使って、多相関数を定義してみます。
以下のようにします。
@TypeChecked class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() } }
引数「Monoid<T> dict=Parameter.IMPLICIT)」が本実装における型クラス制約の宣言であり、意味は、mappendの返り値と、引数で使用する型変数Tは、Monoidという型クラスの制約を満たす必要がある、ということです。
dictは型クラスMonoid型の、型クラスのインスタンスが渡ってくる引数ですが、こちら側から見ると関数の名前空間だと思ってください。引数にあたえられた名前空間に転送します*6。複数の型クラスが引数に与えられたら区別して振り分けます*7。
IMPLICITは以下のように定義してあるただの定数で、AST操作の際のマーカーとして機能します。
class Parameter { static final Object IMPLICIT = null }
ちなみにここで、mappend,memptyが型クラスインスタンスの同名メソッドを呼び出しているだけの関数なのは、無駄に感じるかもしれません。その意味では、ちょっと例がわるくて、Monoid型クラス制約のついた型引数を使った、任意の静的関数が定義できます。ここでmappendやemptyをstatic関数として定義してるのは、利便のため、ユーティティ関数としてこれらをstatic空間にexportしていると思ってください。Haskellだと、インスタンス宣言で、staticな型クラスインスタンスがモジュール内グローバルに存在を開始し、関数呼び出しはそのモジュール内グローバルな空間からマッチする名前が呼ばれることになるんですよね。このGroovy型クラス実装は、Scalaでも同様だと思いますが、名前空間として機能する型クラスインスタンスを明示的に扱うので、1つクッションが入る場合があるということです。善し悪しはあるでしょうが、Haskellの方がシンプルなのは確実です。
(4)型クラスで制約した多相型引数を含む型の引数をとる関数を呼び出す
ここからが、そしてここのみが、山場です。:)
先程定義した関数を呼び出します。以下のようにします。
import static Functions.* : @TypeChecked(extensions='ImplicitParamTransformer.groovy') : String s = mempty() assert s == "" assert mappend("a","b") == "ab"
このコードが静的型チェックのもとでコンパイル・実行できます。
ソースコードの表面上は、関数定義での仮引数Monoid<T> dictに対応する実引数は与えていませんが、暗黙に型クラスのインスタンスが補充されて呼び出されます。これをやってるのが今回作ったImplicitParamTransformerであり、カスタム型チェッカ(型チェッカ拡張)でありながら、型チェッカであることを踏み越えて、以下を実行します。
- ジェネリクス型を使っている静的メソッド呼び出しにおいて、その静的メソッド定義で「IMPLICIT」をパラメータ初期値で使用しているなら、そのパラメータに、ジェネリクス型から定まる変数名を挿入する。呼び出しが、「mappend("a","b")」であるなら、mappendの定義が
static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) }
であるので、実引数の型から型引数の実際の型を推論・解決させて、Tにjava.lang.Stringを束縛して、IMPLICITに対応する引数を、Monoid<String>型のinstand_Monoid_java$lang$Stringという名前の変数の参照に変換します。ついでに、ごく限定されたターゲット型推論を、「String s = mempty()」の形のとき実行し、
static <T> T mempty(Monoid<T> dict=Parameter.IMPLICIT) { dict.mempty() }
では、戻り値が代入される変数の型がStringであることから、Mooid<T>の型がMonoid<String>であると推論させ、変数名を決定します。
最終的には、これらのmappend,memptyは以下の呼び出しと等価になり実行されます。
String s = mempty(instance_Monoid_java$lang$String_ ) assert s == "" assert mappend("a","b",instance_Monoid_java$lang$String_ ) == "ab"
要はScalaのimplicitパラメータの機能を実行していることになります。Scalaの場合は変数名は無意味で型だけで探索するわけですが、特定の型を持った変数定義の探索が面倒なので変数名に型情報をエンコーディングしてスコープ解決させています。
直感的な説明
型クラスのざっくりイメージ的な理解の仕方としては、クラスベースOOPでは、データ型にvtableを組み込み不可分にしていたのを、ひっぺがして、動的な束縛機能を除去して、本当にそれが必要になる最後のタイミング、つまり多相関数呼び出しの時点まで結合を持ち越して、そしてその時点で多相型を解決の枠組みでインスタンスを選定し、別引数で渡し、関数の名前空間として使うということです。
他にやるべきこと
あとはいろんなシンタックスシュガーを導入できると思いますが、学習用としてはここらで打ち止めておきます。
問題点
不都合がありまして、モジュール化ができてません。たとえばEqクラスとかOrdクラスとかそれぞれを別クラスにしたかったのですが、一つの大きな誤算として、多相型のメソッドをstatic importしてもジェネリクス情報が得られません。
class Functions { static <T> T mappend(T t1, T t2, Monoid<T> dict=Parameter.IMPLICIT) { dict.mappend(t1,t2) } :
を、
import static Functions.mappend
すると、引数や返り値のジェネリクス情報が呼び出しコード上の処理段階で不明になります。なので(4)でのジェネリクスの推論ができません。解決策はなんでしょうね。クラスファイルに残っているジェネリクス情報をリフレクションとかでたどるんでしょうかね。たいへんなのでこれ以上の追求は個人的にはあきらめます。
おまけ: Higher-kind Generics
Groovyでは、型引数をとる型構築子を型引数に(いわゆる高階型変数(Higher-kind Generics))したときに期待する動作を行います。たとえば、今回以下のようなコードが書けています。
@TypeChecked interface Applicative<A> extends Functor<A> { public <T> A<T> pure(T t) public <T,R> A<R> ap(A<Function<T,R>> func, A<T> a) // <*> :: f (a -> b) -> f a -> f b }
ここでAは引数を取れる型引数であり、たとえばMaybeやListなどを与えることができます。これはJavaのジェネリクスでは(少なくともJava8では)できないことです 。これができることによって得られる抽象化能力は、非常に大きいものです。モナドはこれがあるから型クラスになり得るのです。
とはいえ、これをもってGroovyでHigher-kind Genericsができる、と言ってよいかどうかは、微妙です。というのも、しなければならないことは「期待する動作を行う」だけではないからです。具体的には、上記を継承するインスタンスで、上記のジェネリクス条件を満たさないオーバーライド定義したときに、型エラーになること、が必要だと思っています。実際試してみると、Groovy 2.4-beta-xでは期待したようにエラーにはならないケースがあるようです。
エラーチェックが正しく出来ないなら、型チェックがなされない=動いてしまう、という結果になるのは明らかです。綿密に調べてもしバグならバグ報告したいところですが、できておりません。
ということで結論は保留です。今のところわかりません。
まとめ
- 型クラスの基本は実装がわかれば理解できる
- ただし言語ごとの実装戦略や、シンタックスの差異は当然あるのでそこは別途。
- 型クラスの構成部品はOOPから流用可能だが、コンセプト的に根本的に相入れないからこそ、型クラスが存在する
- 型クラス=FPではない。その実現に寄与する周辺機能にすぎない。但し静的型および多相型に強く結びついた重要で強力な機能である。FPにおける型クラスは、OOPにおけるクラス継承に比するべき概念である(クラス継承はOOPに必須ではない、ということとも同型)。
- Groovyはコンパイラやランタイムに手を入れずに、カスタム型チェッカやAST変換でかなりのことができる
- 静的型や多相型に関してもある程度のことができる。ただし、静的型チェッカの多相型処理を読み込むのは覚悟がいる。
- Groovy2.xの静的型チェッカの多相型サポートは、激しく進歩している。Javaのそれより強いのは間違いない。
- カスタム型チェッカ(型チェッカ拡張)は、フルのAST変換を書くのにくらべて非常に便利。ただし実行フェイズや、ハンドラを書けるイベントが決まっているので目的とする処理に適合しない可能性がある。
おまけ2:アドホック多相
アドホック多相という言葉は使いませんでしたが、これはこういうことなんじゃないかと思っています(オレオレ理解)。。
- パラメトリック多相: 処理対象のデータ型の特定をしないで処理を記述できるような、処理記述の柔軟性。ただし、処理対象となるデータに対して実施できるのは、ポインタ操作やバイトコピーなど、内容や構造に依存しないような操作のみに限る。典型的にはコンテナへの出し入れ対象として扱うだけ、とか。
- アドホック多相: 目的は同様だが、パラメトリック多相では達成できない、「データの内容に依存した処理」を可能とするために、データと独立した「操作の集合」を事前に定義し、それを関数呼び出し時のデータ引数を渡す時に、引数データの付随情報として与える。(その際に操作の集合の選定を多相型の型推論で元にして自動的に実行する。)
これらの概念は、実装方式とは独立である。たとえばパラメトリック多相についてポインタで実現するケースもあると思うが、型毎に関数実体を適切なタイミングで生成して使っても良いであろう。
*1:パラメトリック多相とは、型引数の制約が無い場合、つまりコンテナに入れる、出す、といった、データ固有の操作を行わない引数を多相にするときに使える。
*2:可能かどうかは実装による。
*3:余談になるが、多言語間呼び出しでは、OOPが長期的には無価値であることが実証されてきたと言うしかない。CORBA/RMIを見よ。SOAPを見よ。WebAPIはメソッドを持たないJSONを返す
*4:@InheritConstructorはすでに似たようなことをしているのでそれを真似すればよい。
*5:Scalaとの相違点:確かScalaでは衝突をコンパイル時エラーにするんじゃなかったかな。
*6:この振り分けは、Haskellなどのイレイジャではないジェネリクス環境で理想的な条件下であれば、静的に決定できるとされている。未確認だが。
*7:Groovyのwith句を使っても良い。
オブジェクト指向だけじゃない開発宣言
Manifesto for Not Only Object-Oriented Development、オブジェクト指向だけじゃない開発宣言、というのが出てましたのでごにょと訳してみました*1。
オブジェクト指向だけじゃない開発宣言
私たちは、ソフトウェア開発の実践
あるいは実践を手助けをする活動を通じて、
よりよい開発方法を見つけだそうとしている。
この活動を通して、私たちは以下の価値に至った。
価値とする。すなわち、左記のことがらに価値があることを認めながらも(但しnullは除くが)、私たちは右記のことがらにより価値をおく。
内容は、「今どきのオブジェクト指向開発宣言」と名乗ってもあんまり違和感ないっす。
ただし、null、おめーは別だ。
追加希望
正格評価よりも、非正格評価を、
チューリングマシンではなく、SKコンビネータを、
追記:この翻訳が本家の日本語訳に採用されました。
Groovy 2.3のtraitをもうちょっと調べてみるついでにScalaのtraitを理解する
(訂正2014/4/17)以下の記事ではGroovy 2.3のtaritはスタッカブルには使えない、と書いておりますが、以下によれば次のベータ(Groovy 2.3-beta-3かな)ではスタッカブルトレイトが利用可能になるようです。
次βでGroovy 2.3のtraitはスタッカブルになるようですヤター RT @CedricChampeau: @tim_yates yes it is. See the other examples in the test case, stackable traits
— uehaj (@uehaj) April 17, 2014
先の記事ではGroovy 2.3のtraitをかるく紹介してみました。
Scalaの比較も試みましたが、なにしろScalaのtraitをあんまり知らないので、「Groovyのtraitにある機能に限っての、Scalaの対応物と比較」という比較になっておりました。その意味で、「Scalaにしか無い機能」というのは比較の眼中になかったのです。
ということで「Scalaスケーラブルプログラミング」のtraitの章を読んでtraitを理解したつもりになったので、本格的な比較をしてみます。
Scalaのtraitは単なる多重継承じゃないよ(という主張)
上記の本のScalaのtraitのとこを読んでると、「単なる多重継承じゃないよ」という主張がひしひしと伝わります。その根拠の一つは、traitでは「super」の意味が拡張されているということのようです。
superの意味が拡張されなければならない理由はごく単純で、素朴な多重継承では「super」が示す親クラスは一つではないからです。多重継承というのは親が複数あるということですからね。かといってsuperを利用不可にしてしまうと、単一継承でのsuperの使い道であるところの「super経由で親のインスタンスを取り出して、superのメソッドを呼び出し、その前後に処理を追加する」という定番のテクニックが使えなくなってしまいます。
Scalaでは、この問題を発展的に解決するために、トレイトの多重継承によって形成されるDAGの要素であるトレイトを、特定のルールに従って線形に並べ直します。線形化すれば、単一継承の様にsuperいっちょうで親を次々とたどっていけるようになります。その順序は良くわかんないものですが、最後に根っこに到達するDAG上の一筆書の経路です。
「発展的解決」の意味は、withの指定順序を替えると処理順序が変わるので、動作のバリエーションが作れるということです。このようなトレイトの用法をスタッカブルトレイトと呼ぶようです。もっとも、試験しなければならないパターンの組み合わせ爆発を考えると、容易に悪夢になり得るとも思うわけですが。
ポイントとして、この線形化経路、すなわち任意のトレイトにおいてsuperが何を指すかは、一つのトレイトを見ただけでは決定できません。あるトレイトが、どうwithで指定されたか(Groovyで言うtraitの複数implements)、によって、順序が変わっていくからです。
trait C extends A with B;
のときBのsuperは(たぶん)Aですが、
trait C extends B with A;
のときのBのsuperは(確か)Cになるでしょう*1。これは、new ... with構文で生成される内部クラスを含め、withを使用してトレイトをミックスインしたクラス定義ごとに、線形化された順序になるようにsuperのチェインを構成・初期化するようなコードないしデータ構造が用意されるのでしょう。
クラス定義に指定されたwithのパターンが、この線形順序をコンパイル時に決定します。superを辿っていくと、この線形化順序で「多重継承上のすべての親」を辿り尽せることが保証されます。superが実際に示す先のインスタンスの型は、実行時にならないと決定されませんので、その意味でsuperは動的です(ただ動的といっても、JavaのメソッドでList型の引数に渡ってくるインスタンスがArrayListなのかLinkedListなのかあるいはそれを継承した他のクラスのインスタンスなのか動的に決まる、のと同じ意味で動的です。「superの型」が抽象クラスかインターフェースになるということです)。
方やGroovyでは
Groovyでは、(トレイトを明示指定しない)superが利用できないので、必要なトレイトを必要な順序で組み替えてsuperを頼りに処理を組み合せるスタッカブルトレイト的に利用することはできませんヨー(2014/04/17追記。冒頭に書いたように次のβでは可能になるようです。ヤター。)。あと、Groovyでは親トレイトの探索順序が違います。例えsuperの参照ができないとしても、「祖父と伯父」の両方で定義されたメソッドのどっちが優先されるか、といった疑問に答えるためには、線形化された順序が定まっているはずです。調べると、Groovyのトレイトは単純な深さ優先探索です。これに対してScalaはとんでもなくわからないルールで決まります。
ちなみにGroovyの親トレイトの探索順序は以下で調べました。
trait A1 {/* String whoami(){"A1"}*/ } trait B1 implements A1 { /*String whoami(){"B1"}*/ };trait B2 implements A1 { /*String whoami(){"B2"}*/ }; trait C1 implements B1 { /*String whoami(){"C1"}*/ };trait C2 implements B1 { /*String whoami(){"C2"}*/ };trait C3 implements B2 { /*String whoami(){"C3"}*/ };trait C4 implements B2 { /*String whoami(){"C4"}*/ }; trait D1 implements C1,C2,C3,C4 { /*String whoami(){"D1"}*/ };trait D2 implements C1,C2,C3,C4 { /*String whoami(){"D2"}*/ }; class E1 implements D1,D2 { /*String whoami(){"E1"}*/ }; E1 e1 = new E1() println e1.whoami() // 上を実行し、表示されたものからwhoami()を削除していくと、その次に到達する // ノードが判るのでそれを繰り返すと親の順序がわかる。結果は以下のとおり // E1 -> D2 -> C4 -> B2 -> A1 -> C3 -> C2 -> B1 -> C1 -> D1 // これは下から深さ優先探索してるのと同じ。
さてここで、@ForceOverrideが指定されたメソッドが1つでもあると、それは@ForceOverride指定されていないメソッドすべてに勝ちます(但しE1を除く。例え@ForceOverrideであろうとも、E1の普通メソッドに勝つことはない。そこまで強くはない。)。@ForceOverride同士では、やはり上と同じように、下から上への深さ優先探索(浅さ優先探索)となるのです。
(追記)
Groovy.2.3正式版では@ForceOverrideはデフォルトかつ必須の動作になり、アノテーション自体は削除されました。
この線形化順序(探索順序)の違いが実用上どういう得失として表われてくるのかというと…ものすごくわからないな。見当もつかないな…。
Groovyでしかできないこと
共通の親トレイトを必要としない形でのトレイト側メソッドの優先
(本節は、2014/4/17訂正あり。Scalaでもself type annotationにより可能とのこと。id:xuweiさん、指摘ありがとうございます。)
Scalaで、スタッカブルを構成するには、組み合せたいトレイト群から構成されるDAG全体の親となるようなトレイト(抽象もしくは具象でも別に良い)が1個必要です。それから、すべてのトレイトだけでなく、末端のクラスがそれを継承していないと、superが作れませんし、トレイトがクラスのメソッドに優先するoverrideにもなりません(いわゆる「abstract override」の話)。
このabstract overrideを使って、ScalaでAOPをやる、という記事がありますが、これだと任意のメソッドをインターセプトできません。インターセプトしたいメソッドを定義したトレイトを作りかつそれをextendsしたクラスに対してしか適用できません。
(次のパラグラフは、2014/04/17追記)
しかし、Scalaには「self type annotation」という機能があり、共通の親トレイトなしでもミックスインによりクラスのメソッドをオーバーライドできます(このときsuperをスタッカブルに使えるかは未確認)。コード例はid:xuweiさんが提示してくれたこちらを参照してください。
これに対応するのですが、Groovyでも、共通の親トレイトはなしで既存クラスに対するオーバーライドができます。置換したいメソッドを持ったクラスに何らかのトレイトをextendsさせるために編集する必要も、ソースコードがある必要もありません。
例えば。
trait FileNameIsHogeHoge { @groovy.transform.ForceOverride String getPath() { return "hogehoge" // (1) } } class MyFile extends File implements FileNameIsHogeHoge { MyFile(String fileName) { super(fileName) } } f = new MyFile("/tmp/file.txt") assert f.getPath() == "hogehoge"
こんな感じ。
泣き言を正直に言えば、(1)でやっぱりsuperを参照したくなることですね。わけですが、次のβを待ちましょう。
今日はこんなところで。
*1:自信なし!!たぶん違うと思う。
Groovy 2.3.0-betaが出たのでtraitを触ってみたメモ
Groovy 2.3.0-beta-1とbeta-2が出たので新機能traitをかるく触ってみました。
注意! 以下は現時点で2.3.0-beta-1と2の振舞いとドキュメントを調べた限りの情報です。正式リリースに向けて変更される可能性があります。
traitの概要と目的
Groovyのtraitは、一言で言って「実装の多重継承」を可能とする仕組みです。詳しくはこちらの本家ドキュメント(英語)をどうぞ。
GroovyおよびJava 7までのJavaでは、インターフェースは多重継承することができましたが、クラスは多重継承できませんでした。実装、すなわちメソッド本体の定義や、非public static finalなフィールド(インスタンス変数)定義はクラスでのみ可能であり、そしてクラスの継承は単一継承のみ(親が1つだけ)が可能なので、実装の継承は、ツリー型に制限されていました。北斗真拳と南斗聖拳の系列があったときDAG型の「両方の継承者」という統合はできないわけです(少なくとも実装の意味では。ジャギは実装は継承してない。上っ面のインターフェースのみです謎)。とにかく、いままでは実装の多重継承はできなかったということです。
Groovyのtraitでは以下の両方ができます。
- クラスのようにメソッド本体やフィールドを定義する
- インターフェースのように多重継承する
結果として、実装の多重継承ができるようになりました。
Java8のインターフェースでは、デフォルトとしての実装(メソッド本体)が定義でき、実装の多重継承もできるので、近いものがありますが、以下のような差異があります。
- groovyのtraitではインスタンス変数(フィールド)も定義できる。つまりtraitで定義されたメソッド群の間でインスタンス固有のデータを共有・保持できる。(Scalaのtraitも同様)
- groovyのtraitは、メソッドのデフォルト実装を定義するだけではなく、親traitのメソッドをオーバーライド定義することもできる。
- groovyのtraitで定義したメソッドは、親クラスや他のtraitのメソッドに優先する(
@ForceOverride使用。(Groovy.2.3正式版では@ForceOverrideはデフォルトかつ必須の動作になりアノテーションは削除された。) Scalaのabstract overrideの動作) - groovyのtraitはJava 8以前のJava VM(Java 6,7..)でgroovyを実行する場合でも利用できる(Scalaのtraitも同様)
GroovyのtraitはScalaのtraitと極めて良く似ています。traitの定義と静的な使用についてはscalaのそれとほぼ同様です*1。差異は、主に動的なtraitの実装に関するところであり、後述します。
表にまとめるとこんな感じ。
Java/Groovyクラス | Java(〜Java7)およびGroovyのインターフェース | Java8以降のインターフェース | Groovyのtrait | Scalaのtrait | |
---|---|---|---|---|---|
定義に使用するキーワード | class | interface | interface | trait | 同左 |
実装の単一継承 | ○ | × | ○(メソッドのデフォルト実装のみ) | ○(フィールド使用・定義およびtrait側メソッド優先も可) | 同左 |
実装の多重継承 | × | × | ○(メソッドのデフォルト実装のみ) | ○(フィールド使用・定義およびtrait側メソッド優先も可) | 同左 |
コード例
たとえばこんな感じです。
trait A {} trait B extends A {} trait C extends A {} class D implements B, C {}
詳しくはドキュメントをみてください。
以下もよろしければどうぞ。
www.slideshare.net
衝突!
さて実装の継承においては、多重継承だろうが単一継承であろうが、衝突というものを考慮する必要があります。
継承というのは親のフィールドやメソッドを引き継ぐということなので、子供は親のメソッドやフィールドを持っているかのように振る舞う必要があります。
単一継承であれば、お爺ちゃんと父で衝突すれば(名前が同じで実装が異なるようなものがあれば)、子供は(子供自身でオーバーライドしない限り)より近い祖先である父のものを持っているかのように振舞います。
多重継承の場合、加えて、父親(もしくはその祖先)と母親(もしくはその祖先)がそれぞれ同名で異なるメソッド実装やフィールドを持っているとき(=衝突)の考慮が必要ですが、子供はどっちのものを持っているかのように振る舞うべきでしょうか。
Groovyのtraitにおける衝突の解決もしくは回避
メソッド名に関しては、Groovyのtraitの衝突解決のデフォルトは指定したトレイトの順に、後勝ちです。つまり「implements 父,母」もしくは後述の形式「withTrait(父,母)」のように複数トレイトを親として指定したときに、所属トレイトを明示指定しないメソッド名が衝突していれば、指定がより後ろである母側のメソッドが指定されたとみなされます。後勝ちが嫌な場合、子供でオーバーライド定義して明示的に後じゃない方を呼ぶこともできます(「トレイト名.super.メソッド名」で指定する)。ちなみにScalaでは潜在的に衝突しているとき、衝突しているメソッドを子供でオーバーライドしないとエラーになり、常に明示的な手動の衝突の解決が求められるそうですが、Groovyではそんな配慮は無いので「意図せざる偶然の衝突」に注意が必要になります。
コード例としてはこういうことです。
trait A { String foo(){"A"}} trait B { String foo(){"B"}} class C implements A,B { } class D implements B,A { } c = new C() assert c.foo() == "B" d = new D() assert d.foo() == "A" class E implements A,B { String foo(){A.super.foo()} } d = new E() assert d.foo() == "A"
フィールドに関しては、フィールドの値を保持する変数名のリネームによって衝突が事前回避されます。トレイトで定義したフィールドは、そのトレイトの実装時に、
トレイトのFQCNの'.'を'_'に置換したもの+'__'+フィールド名
にリネームされた変数名のフィールドが、子クラス側で暗黙に定義されます。フィールドがprivateの場合も、publicの場合もいずれもです。子クラス側のインスタンスのフィールド名を直接指定する場合、後勝ちもクソもなく、このリネームされた名前で常に明示する必要があります。(リネームされる前、トレイト内のメソッドの定義においては、リネーム前の本来のフィールド名をソース記述上は使えます。しかしリフレクションとかでは違う名前になっているでしょうから注意)。
もっとも、フィールドがprivateであれば外部から指定することはできない(建前上不可視*2 )ので、問題になるとすればフィールドがpublicな場合のみでしょう。以下は例です。
trait A { public int a; } trait B extends A { public int b; } trait C extends A { public int c; } class D implements B, C {} def d = new D() println d.A__a println d.B__b println d.C__c
このようなフィールド名のリネームは、やや不自然に感じるかもしれませんが、フィールドを直接参照するのではなくgetter/setterを通じて扱えば、メソッド名の解決の話になりリネームされたフィールド名は隠蔽されるので、プログラマが意識することはなく、実際問題としては意識する機会はあまり多くないでしょう。
重要なのは、このリネームルールから、継承経路上に表われるすべてのトレイト実装は、そのFQCNによって単一化されるということです。ダイヤモンド継承(菱形継承)問題はこの形で解決されています。C++で言えば「仮想基底からの継承(仮想継承)」だけの扱いになるわけです。まあ、それでいいね。なんぼか直観的です。Scalaではどうなるかは知らない。
trait定義
classやinterfaceの代わりにキーワードtraitを使用します。それでだいたい期待通りに動作するでしょう。traitのコンパイル結果は、実体としては(クラスファイル上は)インターフェースといくつかの内部ヘルパクラス群になります。Javaからはトレイトはインターフェースとして見えるので、Groovyのtraitを実装しているGroovyのインスタンスはJavaから扱えます。Groovyのtraitをtraitとして継承したクラスをJavaで定義するのはおそらく無理でしょう(traitを単なるインターフェースとしてJava側でimplementsすることはたぶんできる)。
静的なtrait実装
クラス定義時にトレイトを(インターフェースのように)implementsします。それでだいたい期待通りに動作するでしょう。
実行時のtrait実装
Groovyのtraitは、実行時にそれを実装したオブジェクトを作り出すことができます。Scalaも表面上似たことができるのですが、ここはGroovyとScalaで考え方が一番違うところです。
Scalaの場合を参考に
Scalaでtraitをnew時に実装するには、new..with構文を使用します。この構文は無名内部クラス構文によるインスタンスnewの拡張と言えましょう。無名内部クラスのように、内部的にクラスを生成した上でそのインスタンスを作るのです。Javaの無名内部クラスによるnewではできないこととして、複数のtraitを実装(with)する、対象クラスのサブクラスである無名内部クラスのインスタンスを生成します。これは完全に静的なものです。
たとえば、Xがクラス、Tがtraitだとしたとき
var x:X = new X() with T
であれば、xはXを継承したクラスのインスタンスであり、同時にトレイトTを継承(Scala的にはmixin)したインスタンスです。帰結として、finalクラスにはtraitを実装させることはできません。XがfinalならXのサブクラスが作れないからです。
Groovyでの実行時トレイト実装
Groovyでは、newと独立したタイミングで、既存の任意のインスタンスに対して、traitを実装した新規のプロキシインスタンス*3を作成します。
def x = new X() as T
こうです。トレイトが複数の時はこう。
def x = new X().withTrait(T1,T2)
new Xしてはいますが、そのインスタンスに対するメソッド呼び出しです。だから上はこうも書けます。
def tmp = new X() def x = tmp as T
def tmp = new X() def x = tmp.withTrait(T1,T2)
xはトレイトであるTやT1,T2を実装する、実行時に生成される動的プロキシのクラスのインスタンスです。Scalaとの重要な違いとして、このときのtmpとxはインスタンスが別で、かつxはXのサブクラスのインスタンスではない*4ということです。xはtmpに対するプロキシで、インスタンスのライフサイクルが違うのです。一つのtmpに対して複数回asやwithTraitで複数のプロキシを得ることもできるでしょう。
構文上の類似性があるので混同してしまうかもしれませんが、Scalaのnew時のtrait実装が無名クラス構文によるnewの拡張的な静的なものであるのに対して、Groovyの実行時trait実装はデコレータの生成であると言えます。つまり全然違います。traitの定番(?)の用途であろうDCIへの適用、つまりtraitをロールとして使用する際においては、Groovyの動作もできた方が親和性が高いとワシは思います。
Stringなどのfinalクラスにtraitを注入できる、という点も結果的な差異になります。まあ本当に注入したいのか、というのは置いておいて、ですが*5。
なお、proxyから元のオブジェクトのproxyTargetというプロパティを取得できます(GROOVY-6692とGROOVY-6695)。
落穂拾い
Groovyのtraitは、metaClass.mixinメソッドの上位互換*6的な代替であると見ることもできるかもしれません。metaClassは静的Groovy(@TypeChecked,@CompileStatic)配下では使えませんが、traitは使えるので、静的型チェックに親和性が高いバージョンのmixinと見ることができるかもしれません。さらに憶測ですが、traitの実行時実装の動作で奇妙にも思えるところは、metaClass.mixinのユースケースをカバーするためにそうなっているのかもしれません。
とりあえずおしまい。
*1:たぶん。Scalaは良く知らないので間違いがありましたらご指摘をお願いします。
*2:実際は思い切り見えるがw
*3:この機能は実はインターフェースに関してもともと従来のGroovyにある。「"String" as Runnable」とかやれる!!!。
*4:Groovy 2.3beta1,2では、xに対するメソッドコールはx的にmethodMissingなものについてはtmpに転送されます。そういうディスパッチを行なう「proxy」なのです。しかしXのインスタンスではなく代入互換ではない、さらにDGM非対応とか、mehodMissingなのでXよりもObjectのメソッドequals,hashCodeなどが優先されるとか、静的型チェックには対応できないとか、初学者には混乱を招き得る点がある気もする。
*5:DGMや拡張メソッド、metaClassによるメソッド注入では状態が単純には保持できないので意味あるかといえば意味はある。
Java VM上の言語の覇者を決める(但しJava以外)スクリプトボウルの履歴
米国開催のJavaOneカンファレンスで恒例の人気セッションとなっている「スクリプト・ボウル」というのがあります。Java VM上で動作する言語同士のガチ勝負で、コード例とか、技術力とかいろいろな側面からパネルセッションでバトルして、その年勝者を決める、というものです。
自分では残念ながら直接見たことはないので想像ですが、「朝まで生テレビ」で言語バトルするようなものです。違うか。
エンターテイメント要素も多々あるでしょうし、人気とか「情熱」みたいなのも評価されるかもしれないし、コミュニティの「動員力」とかの要素もひょっとしたらあるかもしれませんね。そういうものも含めて、おもしろい勝負なのではないかと思います。もちろん技術者同士でやってるので、技術的なおもしろみとかもあるでしょう。プレゼンのうまさとかもあるでしょう。
んでその過去の結果をまとめてみました。
年 | 候補 | 結果 |
---|---|---|
2008 | Scala,Groovy,JRuby,Jython | JRuby優勝 リンク |
2009 | Scala,Groovy,JRuby,Jython,Clojure | Groovy優勝 リンク |
2010 | Scala,Groovy,JRuby,Clojure | Groovy優勝 リンク |
2011 | Scala,Groovy,JRuby | ScalaとGroovyが同位優勝 リンク*1 |
2012 | Scala,Groovy,JRuby,Clojure | Scalaが優勝 リンク |
2013 | Scala,Groovy,Nashorn,Clojure | Groovyが優勝 リンク |
圧倒的じゃないか!!
とか言ってみる。
少なくとも皆勤賞ではある。
JavaOne 2012 Tokyo/JVM言語BOF
JavaOne Tokyoに参加しています。
表記のBOFにはスピーカーの一人として参加致しました。資料はこちら。
直前の暴風雨で、予定していた懇親会が流れたせいで(?)、慣れ合い/筋書き/打ち合せなしのガチンコ勝負へ…。
@yuyoroさんの究極の秘密兵器とその崩壊、唖然とさせるダッシュボード、「Javaは○○だし」暴言などなど、個人的にも最も楽しかったセッションでした。
客観的に見ると、マイナー言語同士で争ってどうすんだ、って話ではありますが、id:nobeansさんの奮闘の甲斐あり、Groovyがめでたく1位に(ぱちぱち)。でも、さすがの@nahiさんによるJRuby with indyの性能は素晴しい。Scalaのパターンマッチうらやましい。@tmizuさんの冷静なつっこみ、id:kiy0taka さんの司会もすばらしす。奥さんの投票アプリ*1はGroovyFXというのも加点ポイントだったか。LTでは明らかに理解している人が僅少であろう型プログラミングの話題や、Jrubyのコミッターはいい人(これが唯一の和む話題)とか、Oracleの牙城でうっかり禁句のAndroidのgradle試験の話題をだす@kyon_mmさん、などみんな楽しい。これがBOFの醍醐味かと。@toby55kijさんの的確な時間管理のおかげで流れも良く終りました。みなさん、おつかれさまでしたありがとう。
次回は完膚なきまでに打ちのめしますのでまたやりましょうw
だれかTogetterでまとめてくれないかのう。
(追記)
id:orangecloverさんがまとめてくれました。たいへんありがとうございました。
http://togetter.com/li/283845
*1:この堅牢たる高可用性・ハイスケーラブルアプリの裏話はLTで明らかになりましたが、愕然。
GroovyServの小技シリーズ2 scalacを高速化する
つい昨日0.4がリリースされたGroovyServですが、相当バグフィックス入ってるのでお薦めです。
GroovyServはGroovyスクリプトの起動を高速化するものですが、Groovyから呼び出すものはGroovyに限りません。つまりJVM上で動くものならなんでも起動の高速化が理論的には*1可能です。
たとえば、scalaの処理系とかscalaで書かれたプログラムもその例外ではないです。
scalaには、scalacというコマンドがあってscalaコードをコンパイルします。まずscalacを常駐させて起動を高速化してみましょう。
その前にオリジナルのscalacの実行速度。(Mac OS X, Scala 2.8.0-final使用)
コンパイル対象(hello.scala)は以下の通り。
object HelloWorld { def main(args: Array[String]) { println("Hello Scala World!") } }
scalacをGroovyServ上で常駐実行させた場合
これをGroovyServから呼び出してみます。前準備としては以下のようにscalaのjarを~/.groovy/lib/にコピー。
CLASSPATH通してもいいんだけど面倒なんで。
さて、scalacをGroovyServで実行してみますと。
$ time groovyclient -Dscala.home=$SCALA_HOME -e 'scala.tools.nsc.Main.main(args);' -- hello.scala
real 0m1.256s
7秒が1秒少しに短縮されてます。なかなかです。あ、やってることはscalacのシェルスクリプトの中身を除いて、main()を含むクラスを呼び出しているだけです。
fscの場合
さて、当然ここでfsc(Fast Scala Compiler)はどうなの、という話になりますね。
fscは、scalacを常駐させてコンパイルを速くするものです。GroovyServと似てますが、scalaソースコードのコンパイル専門であり、かつクライアントもJavaじゃなくてScalaか、で書かれているという違いがあります。
やってみます。
$ time fsc hello.scala
real 0m1.071s
1秒ぐらいですね。GroovyServがやや負けてますが、まあ「汎用対専用」ということで、汎用としてはいい線いってるんではないでしょうか。
fscをGroovyServ上で常駐実行させた場合
以上。とも思ったのですが、ここで終わると記事にならないので考える。
・・・いやまてよ、fsc自体Pure JavaのというよりPure ScalaのJVM上で動くプログラムなんだから、fscをGroovyServで常駐させたらどうなるんだろう?ということで実験。
$ time groovyclient -Dscala.home=$SCALA_HOME -e 'scala.tools.nsc.CompileClient.main(args);' -- hello.scala
real 0m0.337s
0.3秒台!爆速達成!
このぐらい違うと体感でかなりちがってきます。
ちなみにJVMプロセスが2個常駐していることになります。
考察
高速化した理由は、推測ですが,初期化処理も分離されるからだと思われます。
scalacは、
- 1. 起動
- 2. 初期化
- 3. コンパイル
というようにつくられていて、GroovyServだけでは1が削減されるだけなので2,3は減らない。
fscでは、2がスキップされるように作られているが、1,3は減らない。
GroovyServとfscを組み合わせることで、1,2が削減され、3だけ実行するようになります。
(゚Д゚)ウマー
以降は、できてもいないなのであまり良くないことですが、可能性としてだけ。あくまで可能性ですが、うまいことGroovyコード断片を書いて、都度は3から実行できるようにすれば、GroovyServだけで同等の高速化ができる可能性があります。柔軟なGroovyであるがゆえです。ここらへんがNailGunとの差別化ポイントとなるところの1つです。できないかもしれませんが。