uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Groovy 2.3のtraitをもうちょっと調べてみるついでにScalaのtraitを理解する


(訂正2014/4/17)以下の記事ではGroovy 2.3のtaritはスタッカブルには使えない、と書いておりますが、以下によれば次のベータ(Groovy 2.3-beta-3かな)ではスタッカブルトレイトが利用可能になるようです。

先の記事ではGroovy 2.3のtraitをかるく紹介してみました。

Scalaの比較も試みましたが、なにしろScalaのtraitをあんまり知らないので、「Groovyのtraitにある機能に限っての、Scalaの対応物と比較」という比較になっておりました。その意味で、「Scalaにしか無い機能」というのは比較の眼中になかったのです。

ということで「Scalaスケーラブルプログラミング」のtraitの章を読んでtraitを理解したつもりになったので、本格的な比較をしてみます。

Scalaスケーラブルプログラミング第2版
Martin Odersky Lex Spoon Bill Venners
インプレスジャパン
売り上げランキング: 29,544

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を参照したくなることですね。わけですが、次のβを待ちましょう。

今日はこんなところで。

Scalaスケーラブルプログラミング第2版
Martin Odersky Lex Spoon Bill Venners
インプレスジャパン
売り上げランキング: 29,544

*1:自信なし!!たぶん違うと思う。