enumに継承を! traitとenumの妙な関係、もしくはGrailsのドメインクラスの選択フィールドを国際化表示するのにtraitが便利
Grailsのドメインクラスにおいて、いくつかの候補の数値のいずれか、というフィールドを作成し、scaffoldで生成した画面からCRUD操作したいとします。
簡単なのは、こうですね。
class Domain { Integer something static constraints = { something inList:[1,2,3] } }
しかし数値フィールドに量としてではなく個々の値にそれぞれの意味がある場合、たとえば、腹の状態を表すフィールドの値として、
- 1: はらぺこ
- 2:まんぷく
- 3:こばらがへった
を表現するようなものだったとします。このとき、すくなくとも、Scaffoldの画面上では数値ではなく意味のわかる文字列で表示したり選択入力させたいわけです。しかしデータそのものを文字列にするのもDB上の表現をそうしたくないのでいやだとします*1。
ということでenumの出番です。
class Domain { enum OnakaStatus { STARVATION(1), SATICEFIED(2), SOSO(3) private final int id; private OnakaStatus(int id) { this.id = id; } public int getId() { return id; } } OnakaStatus onakaStatus static constraints = { // onakaStatus inList:[OnakaStatus.STARVATION,OnakaStatus.SATICEFIED,OnakaStatus.SOSO] // よく考えるとこんなconstraintはいらない。enumなんだから。 } }
"STARVATION","SATICEFIED","SOSO"を選択肢とするセレクト/オプションタグをscaffoldが生成し、FORMパラメータとしてはその文字列、DB中には対応する数値(1,2,3)が保存されます。ナイス! GORMナイス!
でもこれだと表示が日本語にならないし。"STERVATION"の代りに"はらぺこ"というenumの識別子を使えば表示が日本語にはなるが、FORMパラメータとしてその日本語文字列が使われるのは好きくない。ではこうしてみてはどうか。
class Domain { enum OnakaStatus { STARVATION(1, "はらぺこ"), SATICEFIED(2,"まんぷく"), SOSO(3,"こばらがへった") private final int id; private final String value private OnakaStatus(int id, String value) { this.id = id; this.value = value; } public int getId() { return id; } String toString() { return value } } OnakaStatus onakaStatus }
一応これで識別子は英字のまま表示を日本語にできます。しかしながら、国際化対応ができません。
ぐぬぬ。
そういう場合は、enumにorg.springframework.context.MessageSourceResolvableを実装させて、以下のメソッドを定義し、
enum OnakaStatus implements org.springframework.context.MessageSourceResolvable { : public Object[] getArguments() { [] as Object[] } public String[] getCodes() { [ name() ] } public String getDefaultMessage() { "?-" + name() } }
この上でi18nメッセージをenumの要素をキーとして用意します。たとえば、grails-app/i18n/なんとか.propertiesに、
STARVATION=はらぺこ SATICEFIED=まんぷく SOSO=こばらが減った
でも、enumごとにメソッド追加するの〜! えー冗長。やだーやだー。
ぐぬぬぬ。
共通するメソッドを親クラスに切り出したいところですが、enumはクラスからの継承(extends)はJava/Groovyの言語仕様上許されておりません。interfaceからのimplementsなら可能なのだが。はっ!!
ということでtraitを使えばいいのです。
trait I18nEnum implements org.springframework.context.MessageSourceResolvable { public Object[] getArguments() { [] as Object[] } public String[] getCodes() { [ name() ] } public String getDefaultMessage() { "?-" + name() } }
こういうtraitを用意して、enumはこうして
enum OnakaStatus implements I18nEnum { STARVATION(1), SATICEFIED(2), SOSO(3) private int id; private OnakaStatus(int id) { this.id = id; } public int getId() { return id; } }
あらすっきり*2。
enum継承できないのでメソッドを共有させることが本来はできないわけですが、implementsはでき、試したところtraitからもできた。なのでenumでメソッドを共有化できました。めでたい。Groovy便利。
Java8のデフォルトメソッドでどうかは不明。
関連記事
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によるメソッド注入では状態が単純には保持できないので意味あるかといえば意味はある。