LL脳がscalaの勉強を始めたよ その37


Scalaコップ本のトレイトに関する12章の後半をやっていきますよー

前回はトレイトを使ったリッチインターフェースの作成についてやってのですが、今回は積み重ね可能な変更や多重継承との違いについてやっていきますねー

積み重ね可能な変更をそれぞれのトレイトで表現する

トレイトの使い方その2として積み重ね可能な変更(stackable modification)についてやりますよー。英語で書いたのはちょっと日本語だけだと不安だったからですが…そのままですな。

積み重ね可能な変更って?

ざっくりと言ってしまえばトレイトの重ね掛けで異なる性質を引き出してしまおうぜベイベーな感じです…っていうのはざっくりしすぎですね

要するにトレイトをミックスインすることで機能を追加したりすることができるけども、トレイトをミックスインでは機能の拡張も出来るよ(・∀・)しかも複数ミックスインすることで同じメソッドを次々と拡張して多種多様(複雑)な効果を産み出してしまうよ(`・ω・´)ってことっぽいです…多分ね

字面だけみるとこんな感じ?

薬品A(大元のクラス) → 薬品B(トレイト) → 薬品C(トレイト) → 素敵なアレ(ってなればいいなぁ...)

とりあえずサンプルやって手を動かしてみますかねー

サンプル用のクラスを組み立てますよー

サンプルとして整数待ち行列の処理クラスを考えますよー

クラスの持っているメソッドは「整数の追加:put」と「整数の取り出し:get」の2つで、待ち行列FIFO(先入れ先出し)方式になりまスネ

とりあえずこんな感じで定義してみますよー

// 抽象クラスを定義します
abstract class IntQueue {
  def get():Int
  def put(x:Int)
}

// ArrayBufferを利用して具象クラスを定義しますよ
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
  // 待ち行列を定義しますね(これは変更可能[mutable]です)
  private val buf = new ArrayBuffer[Int]
  // 取り出しメソッドを実装します
  def get() = buf.remove(0)
  // 追加メソッドを実装します
  def put(x:Int){ buf += x } 
}

実際に使ってみますよー

// オブジェクトを生成デス
scala> val queue = new BasicIntQueue
queue: BasicIntQueue = BasicIntQueue@1d648e2
// 追加します
scala> queue.put(10)
// 更に追加します
scala> queue.put(20)
// 取り出します
scala> queue.get
res4: Int = 10
// さらに取り出します
scala> queue.get
res5: Int = 20
// ないハズのところから取り出します
scala> queue.get
// ネーヨ(゚Д゚)って怒られました
java.lang.IndexOutOfBoundsException: cannot remove element at 0
	at scala.collection.mutable.ArrayBuffer.remove(ArrayBuffer.scala:148)
	at BasicIntQueue.get(<console>:10)
	at .<init>(<console>:9)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Nat...
scala> 
さてとりあえず積み重ね可能な変更してみますかね

整数を追加する場合に2倍にしてから待ち行列に加えるようなトレイトミックスインをしてみましょうー

まずはトレイトの定義デス

// 待ち行列内の値を2倍にしますよ
trait Doubling extends IntQueue {
  // putメソッドを拡張(追加前に2倍)するのです
  abstract override def put(x:Int){super.put(2 * x)}
}

上記記述方法で2点ほど。

まずIntQueueをextendsすることで「IntQueueを拡張するクラスのみにミックスインを許しますよ」という宣言になるそうです。なのでBasicIntQueueとかにはミックスインできるけど適当に作ったクラスHogeとかにはできないみたいですね。

// 無理やりミックスインしようとしたら怒られました(´・ω・`)
scala> class Hoge(x:Int) extends Doubling {
     |   def put = x * 2                   
     | }
<console>:6: error: class Hoge needs to be a mixin, since method put in trait Doubling of type (Int)Unit is marked `abstract' and `override'
       class Hoge(x:Int) extends Doubling {
             ^

// かといってextendsしないと引数何?って怒られます…当たり前ですね(´・ω・`)
scala> trait Doubling {                                    
     |   abstract override def put(x:Int){super.put(2 * x)}
     | }
<console>:5: error: value put is not a member of java.lang.Object with ScalaObject
         abstract override def put(x:Int){super.put(2 * x)}
                                                ^

またabstract宣言されたメソッドでsuper呼出をしているんだけども、トレイト内のsuperの呼出は動的に束縛されるので、該当メソッドを具象定義しているクラスやトレイトのあと(afterメソッド的な扱い)ならば無問題とのこと。それとabstract override宣言で「変更の積み重ねを実装する」ということをコンパイラーに明示的に宣言しているそうです。

…とりあえず動かしてみましょうか

// Doublinを重ね掛けしたサブクラスを用意します
cala> class MyQueue extends BasicIntQueue with Doubling 
defined class MyQueue
// オブジェクト生成します
scala> val queue = new MyQueue                          
queue: MyQueue = MyQueue@546169

// 追加します
scala> queue.put(10)
// 取り出します、ちゃんと2倍されてます
scala> queue.get              
res8: Int = 20

上の例だとBasicIntQueueでputメソッドが具象化されているのでDoubling内のabstractでのsuper呼び出しがきちんと動作するわけですねー

例えばこんな感じで抽象クラスのみの先行呼出だと怒られヤス

// IntQueueは抽象メソッドしかないので怒られました
scala> class MyQueue extends IntQueue with Doubling     
<console>:6: error: class MyQueue needs to be a mixin, since method put in trait Doubling of type (Int)Unit is marked `abstract' and `override'
       class MyQueue extends IntQueue with Doubling
             ^
いちいちクラスを作るのはめんどくさい(´・ω・`)

使い捨てのミックスイン用にクラスを作るのはエコじゃないよね、ってことでnew時のミックスインもやってみましょう

// インスタンス生成時にwithキーワードで呼び出すだけデス
scala> val queue = new BasicIntQueue with Doubling
queue: BasicIntQueue with Doubling = $anon$1@9c15d2

// ちゃんと2倍されてますねー
scala> queue.put(10)
scala> queue.get    
res10: Int = 20
重ね掛けの本領発揮すんぞ(`・ω・´)

複数のトレイトミックスインもやってみますよー、まずは積み重ね可能な変更トレイトとしてIncrementingとFilteringの2つを定義しますー

// 待ち行列の値に1加えるトレイトです
trait Incrementing extends IntQueue {
  abstract override def put(x:int){super.put(x +1)}
}

// 待ち行列の値から負の数を取り除くトレイトです
trait Filtering extends IntQueue {
  abstract override def put(x:int){ if(x >= 0) super.put(x)}
}

さて適用してみますかねー

// 2つのトレイトをミックスインしてみます
scala> val queue = new BasicIntQueue with Incrementing with Filtering 
queue: BasicIntQueue with Incrementing with Filtering = $anon$1@1f7be7b

// -2, -1, 0の3つの要素を突っ込みます
scala> queue.put(-2);queue.put(-1);queue.put(0);                     

// 取り出します
scala> queue.get                                                     
res13: Int = 1

// ... 要素がもうないデス
scala> queue.get
java.lang.IndexOutOfBoundsException: cannot remove element at 0
	at scala.collection.mutable.ArrayBuffer.remove(ArrayBuffer.scala:148)
	at BasicIntQueue.get(<console>:10)
	at .<init>(<console>:11)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Na...
scala> 

どうやら複数ミックスインは後ろ(Filterling)から順に適用されているみたいですね。まず-2, -1が負の数なので排除されて、残った0のみが1加えられている…といった感じですね。

複数トレイトミックスインは順序も大事

今度は逆に適用してみますかね

// Incrementing、Filteringの順に適用されるはず
scala> val queue = new BasicIntQueue with Filtering with Incrementing
queue: BasicIntQueue with Filtering with Incrementing = $anon$1@95f75
// 値を突っ込みます
scala> queue.put(-2);queue.put(-1);queue.put(0);                     

// 元-1が取り出されマシタ
scala> queue.get                                                     
res15: Int = 0
// 元0が取り出されマシタ
scala> queue.get
res16: Int = 1

// もう値が無いですね
scala> queue.get
java.lang.IndexOutOfBoundsException: cannot remove element at 0
	at scala.collection.mutable.ArrayBuffer.remove(ArrayBuffer.scala:148)
	at BasicIntQueue.get(<console>:10)
	at .<init>(<console>:11)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Na...

FilteringとIncrementingの適用順で処理結果が異なってしまいました。

順序の違いで処理結果が変わる多様性

適用順序を変更することで処理結果が変わるなら3つのトレイトのミックスインの方法次第でで16通りの異なる処理を行うクラスを定義することが出来るのです(;゚Д゚)(゚Д゚;(゚Д゚;)ナ、ナンダッテー!!、まあ冗談抜きで部品の組み合わせ方で様々な振る舞いを決定できるので再利用性は異常に高くなりそうですな…まるで錬金術みたい(´・ω・`)

ちなみに16通りの内訳は3トレイトの順序で6通り、2トレイトの組み合わせ順序で6通り、1トレイとの選択で3通り、トレイト無しで1通りです

ただ、きちんとしたトレイトミックスインレシピが存在していないと現場には混乱の渦になるような…まあトレイトをミックスイン済みクラスを継承して使うような運用で回避できるっちゃー出来るのか…まあ、それ以上にコア設計者(トレイト周辺の担当者)にはそれこそ錬金術師的なスキルが求められるような気がするデスネ。下手な設計されると現場が阿鼻叫喚的な(´・ω・`)

便利なものは反転するととてつもなく攻撃力を持った自爆兵器になるいい例になりそうですな…うん、ちゃんと考えます。

Scalaが多重継承ではなくてミックスイン合成を選んだ理由

多重継承だと継承元両方に同じメソッドがあった場合に、その両方を適用することが難しいよね、だからScalaは線形化の行えるミックスイン合成を選択したのさ( ー`дー´)キリッ(超意訳)

多重継承っぽいものを確認してみますよ

継承元の同名メソッドを両方適用できないことを多重継承風コードを書いて確認してみますよ

まずは順当に…

// 多重継承のコードだと思いねぇ
val q = new BasicIntQueue with Incrementing with Doubling
// これどっちのputが適用されるのん?
q.put(42)

上の例だと多重継承の規則(最後or最初に定義されたものが適用されるトカ)次第で振る舞いが変わりますねー

なので強引に両方が適用されるっぽいコードを書いてみた、Scala的に動かないけドモ

tarit MyQueue extends BasicIntQueue with Incrementing with Double{
  def put(x:Int) {
    //  IncrementingとDoublingが独立にうごいて
    // 待ち行列に勝手に値が追加されるので
    // これだときちんと適用されないデス(´・ω・`)
    Incrementing.super.put(x)
    Doubling.super.put(x)
  }
}

こんな感じで最初の方でやった重ね掛けが実現できないからScalaは多重継承を捨ててミックスイン合成を選んだみたいですね…多分

ミックスイン合成のための線形化

ちらちら出ている線形化について、線形化のふるまいを例を使ってまとめてみますよー。多分俺俺用語の”重ね掛け”が線形化的内容を指していると思われマス

ちなみに線形化順序は指定されたクラス・トレイトが最初になって、その後はミックスインや継承関係で順序が決定されますねー

例えば次のようなクラス・トレイト群があるとします

// スーパークラス
class Animal

// Animalクラス関連のトレイト
trait Furry extends Animal
trait HasLegs extends Animal
trait FourLegged extends HasLegs

// 多分具象クラス?のニャッハー
class Cat extends Animal with Furry with FourLegged

まずは大本のAnimalの線形化順序はこうなりますねー、Scalaの階層構造の頂点につながります

Animal → AnyRef → Any

次にFurryトレイトはこんな感じ

Furry → Animal → AnyRef → Any

HasLegsを継承しているFourLegsはこんな感じですねー…コップ本ではFurryが入っているけど印刷ミスじゃないかと思います…

FourLeggeds → HasLegs → Animal → AnyRef → Any

Catだとこんな感じですねー

Cat → FourLegged → HasLegs → Furry → Animal → AnyRef → Any

基本的に自分から始めて後ろから前に適用されていく、って感じで解釈すればいいですかね?ついでに各クラス・トレイトでのsuper呼出は上の線形化順序の右側(ミックスイン・継承定義の場合は後ろ:上の例ではCatでのsuperはFourLegged)が呼ばれるって覚えておくと混乱が少なくなりそうですねー

トレイトすべきか、せざるべきか

コップ本的トレイト利用のガイドラインを羅列しますよー

  • トレイトにする場合
    • 複数の無関係なクラスから利用される可能性がある
      • クラス階層の異なる部分にミックスインできるのはトレイトだけらしいデス
    • 下記以外で悩んだ場合(頑張ってみる(`・ω・´))
      • そのほうが選択肢が広がりますよーだってさ
  • トレイトにしない場合
    • 振る舞いが再利用されない場合 → 具象クラスで作成
      • だって再利用されないし
    • Javaコードを継承できるようにしたい場合 → 抽象クラス
      • 実装つきのトレイトに類似したものがJavaにないので変換できないそうです。抽象メンバーだけならOKだって
    • ライブラリーをコンパイル後の携帯で配布したい場合 → 抽象クラス
      • トレイトのメンバー構成が変わったりすると継承しているクラス自体(コンパイル済みクラス)に変更がなくても再コンパイルが必要になるので
    • 処理効率が非常に重視される場合 → クラス
      • トレイトはJVM用にインターフェースとしてコンパイルされるけども、多くのランタイムではインターフェースメソッドがクラスメソッドに比べて遅くなるので

とりあえずこんな感じみたいですね。まあ、上記条件に当てはまらないときはできるだけトレイト使う方向で頑張ってみますかねー

以上

トレイトについて駆け足でやっていきましたよー、正直トレイト楽しい。トレイトと積み重ね可能な変更が使いこなせるようになると素敵なコードがかけそうなのでワクワクですな。頑張ろう(`・ω・´)

次回からは13章のパッケージとインポートに進みますよー