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


Scalaコップ本の第12章に入りますよー、12章はScalaの特徴ともいえるトレイトについてですよー

トレイトはScalaにおけるコード再利用の基本単位でメソッド定義やフィールド定義をカプセル化したものだとかなんだとか…まあ、頑張っていきましょうかねー

トレイトの仕組み

トレイトの定義はtraitキーワードを使う以外はクラス定義とよく似ているそうデス。とりあえずこんな感じですかねー

// 哲学的な形質(trait)を定義しますよ
trait Philosophical {
  // 哲学的な思索をしますよ
  def philosophize(){
    // 我メモリを消費する、故に我あり
    // 「我思う、故に我あり」のコンピュータ版かしら?
    println("I consume memory, therefore I am!")
  }
}

定義されたトレイトはスーパークラス宣言がないのでデフォルトのAnyRefをスーパークラスとして持ちますね。逆を言えばトレイトもスーパークラスを持てるみたいデス。ほんとクラスと同じような感じですなー

ミックスインします

実際にトレイトを使ってみることにしましょうか。トレイトはクラスにミックスイン(ミックスイン合成)して使うことになりますので…まずは形だけでもイロイロとやってみましょう。

ちなみにミックスインはextends、extendsを既に使っている場合はwithキーワードで行ないますね(複数ミックスインは後ろの説でやりますよ)

// 蛙クラスに哲学的形質を与えてみますよ
class Frog extends Philosophical {
  override def toString = "green"
}

// 蛙に哲学させてみますよ
scala> val frog = new Frog
frog: Frog = green
// トレイトのメソッドを実行しますよー
scala> frog.philosophize  
// なんか哲学っぽいことをいいました(゚∀゚)
I consume memory, therefore I am!

// 一つだけミックスインする場合はwithじゃダメみたいです
class Frog with Philosophical {
  override def toString = "green"
}
// withダメって怒られました
<console>:1: error: ';' expected but 'with' found.
       class Frog with Philosophical {
                  ^

…単純なミックスインだとあまり継承と変わらないっすね…ちなみに、ミックスインされたクラスはトレイトのスーパークラスはミックスインしたトレイトのスーパークラスのサブクラスになるみたいですね。

例えばFrogクラスはextendsでPhilosophicalトレイトをミックスインしているのですが、結果的にPhilosophicalクラスのスーパークラスAnyRefクラスのスーパークラスになるですね。

スーパークラスを持つクラスにミックスイン

親クラスを持っているクラスに対してもトレイトミックスインができますネ。その場合は継承時にextendsキーワードを使っているのでwithキーワードを使いますよ

// 親クラスデス
class Animal
// サブクラスにミックスインします
class Frog extends Animal with Philosophical {
  override def toString = "green"
}

…extendsを継承用にしてミックスインのときはwithだけにすればいいのに…と思ったのは淺薄なのかしら(´・ω・`)

複数のトレイトをミックスインです

Scala的多重継承の本領発揮の複数トレイトミックスインをやってみますよー、複数ミックスインする場合はwithキーワードを繰り返しで使いますねー

// 親クラスデス
class Animal
// トレイトをもう一つ定義しますヨ
trait HasLegs
// サブクラスに複数ミックスインします
class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}
トレイトのメソッドをオーバーライドしてやりますよ

ミックスインしたクラスではトレイトで実装されたメソッド(上の例だとphilosophize)を継承して使っておりますが、こいつをオーバーライドすることも可能みたいですねー

// 親クラスデス
class Animal
// サブクラスにミックスインします
class Frog extends Animal with Philosophical {
  override def toString = "green"
  // トレイトのメソッドをオーバーライドしてやりますよー
  override def philosophize(){
    // 哲学的セリフを変えてやりますかねー
    println("It ain't easy being " + toString + "!")
  }
}

// 実行します
scala> val frog = new Frog
frog: Frog = green
scala> frog.philosophize
// 緑でいるのも簡単じゃねーんだよ( ー`дー´)キリッって蛙っぽいなぁ
It ain't easy being green!
トレイトで型定義

トレイトはクラスと同様に型の定義も出来るみたいですねー

// トレイトPhilosophical型の変数にFrogオブジェクトを突っ込んでみましたよ
scala> val phil:Philosophical = frog
phil: Philosophical = green

// でも上でメソッドがオーバーライドされてたので
// 実行されるphilosophizeは蛙風味です
// (オーバーライドされてなければトレイトのものが実行されます)
scala> phil.philosophize
It ain't easy being green!
結局トレイトって何者よ?

ここまでやってみてトレイトが一体なんなのか…ってことですが。Javaのインターフェースが具象メソッドを持てたりフィールドを持ったりできるようになってパワーアップしたもの…ととることができるみたいデス。まあ、そんなものは既にJavaインターフェースではないわけですが…

ぶっちゃけトレイト自体は次の2つの性質以外はクラスと全く同じなので、ミックスイン専用クラスぐらいに考えておくのがいいのかもです。

トレイトとクラスの違い その1:クラスパラメーターがとれません

トレイトはクラスパラメーターがとれません。うん、書いちゃうとそれまでなんだけど

// クラスに渡すパラメーターがクラスパラメーター
scala> class Point(x:Int, y:Int)
defined class Point

// トレイトでは怒られますねー
scala> trait NoPoint(x:Int, y:Int)
<console>:1: error: traits or objects may not have parameters
       trait NoPoint(x:Int, y:Int)

シンインターフェースとリッチインターフェース

トレイトの用途の一つとして、クラスが既に持っているメソッドを使って自動的にメソッドを増やすものがあるみたいです。コップ本的にはシン(貧弱)インターフェースをリッチインタフェースに変えることができるのです(;゚Д゚)(゚Д゚;(゚Д゚;)ナ、ナンダッテー!! だそうです

シンインターフェースとリッチインターフェースのトレードオフ

シンインターフェースにするかリッチインターフェースにするかはオブジェクト指向での設計時の悩みどころの1つみたいです。

例えばシンインターフェースであれば、インターフェース実装者は(ほとんど実装しなくて良いので)楽できるもののクライアント側での実装が増えるわけですね。逆にリッチインターフェースを実装する場合はインターフェース側で大量のメソッドを実装しなければならない事になるわけですねー

Javaのインターフェースは(コップ本では4つのメソッドしか持たないCharSequenceを例にしてますが)シンインターフェースであることが多いらしいんですが、Scalaのトレイトは具象メソッドを持つことができるのでよりリッチなインターフェースを作ることができるみたいデス。

既に実装されたメソッドであれば使い回しも楽そうですしねー(Javaのインターフェースは抽象メソッドしかもたないらしいので…)

ここまでの知識だけでトレイトはどういうもの?

とりあえず適当な考察をして自分なりに整理してみる。

Scalaでのトレイトは実際に用いられる部品的なオブジェクトで、各クラスではそれらの部品をうまい具合に組み合わせて実際の振る舞いを決定するみたいな感じかしら?

なにがしかのモノを作るという活動で考えたら次のようになるのかしらね?

  • クラス:作業場所とその操作
  • トレイト:道具箱

…そういやtwitterで継承と合成について教えてくれた方も道具箱の例をだしてたなぁ

だとするとScalaによる開発ではトレイトの作成⇒クラスの作成って進むのが良いのかしら?まあ、とりあえずまだ前半なのでコレくらいにして先に進んでいきましょうかねー

サンプル:矩形オブジェクト

トレイトを利用したリッチインターフェース的サンプルをやってみますよー

サンプルの内容は矩形オブジェクトを描くグラフィックライブラリーです

まずはトレイトを使わない実装です

トレイトを使わないとめんどくさいってのを実例してみますよー

まずは基本幾何学クラスのRectangleクラスを実装しますよー

// 座標を表すオブジェクトです
class Point(val x:Int, val y:Int)
// 矩形オブジェクトですねー、左上と右下の点座標を引数にとりマス
class Rectangle(val topleft:Point, val bottomRight:Point){
  // 左座標デス
  def left = topLeft.x
  // 右座標デス
  def right = bottomRight.x
  // 座標から幅を求めますよー
  def width = right - left
  // その他の幾何学的お役立ちメソッドを大量に....
}

んで、実装対象のグラフィックライブラリーは次のような2Dウィジェットクラスも持ちますのでそいつも作ってみますよー

abstract class Component {
  // 左上、右下の点座標を定義しマス
  def topLeft:Point
  def bottomRight:Point
  // 左座標デス
  def left = topLeft.x
  // 右座標デス
  def right = bottomRight.x
  // 座標から幅を求めますよー
  def width = right - left
  // その他の幾何学的お役立ちメソッドを大量に....
}

…かなり重複しまくりデスね…トレイト使って整理しましょー

トレイト使ってやんぞ

まずは上のほうで重複している部分をトレイトにまとめますよー

今回作成するトレイトは左上・右下の各点を返す抽象メソッドと位置や幅やその他もろもろの値を具体的に処理して返す具象メソッドを持っております

trait Rectangular {
  // 左上、右下の点座標を定義しマス
  def topLeft:Point
  def bottomRight:Point
  // 左座標デス
  def left = topLeft.x
  // 右座標デス
  def right = bottomRight.x
  // 座標から幅を求めますよー
  def width = right - left
  // その他の幾何学的お役立ちメソッドを大量に....
}

んじゃComponentクラスにミックス☆インします

abstract class Component extends Rectangular {
  // Component独自のメソッド…
}

Rectangleクラスにもミックスインしますよー

class Rectangle(val topLeft:Point, val bottomRight:Point) extends Rectangular{
  //  Rectangleクラス独自のメソッド…
}

ミックスインしたクラスを実際に使ってみますかねー

//  Rectangleオブジェクトです
scala> val rect = new Rectangle(new Point(2,3), new Point(8,9))
rect: Rectangle = Rectangle@b103dd

//// トレイトの各メソッドが呼び出されておりますねー
// 左位置取得デス
scala> rect.left
res5: Int = 2
// 右位置取得デス
scala> rect.right
res6: Int = 8
// 幅取得デス
scala> rect.width
res7: Int = 6

おお、出来ましたー

Orderedトレイト

Scala提供のトレイトも使ってみますかねー、比較演算系の機能を提供するOrderedトレイトですよー

トレイトないバージョン

まずは先程と同様にトレイトが無いパターンを書いてみますねー。6章で作成した有理数演算用クラスRationalを基にして、こいつに< , > <=, >=の4つの比較演算子を組み込んでみますよー

// 6章で実装した有理数演算用クラスです
class Rational(n:Int, d:Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer = n / g
    val denom = d / g
    def this(n:Int) = this(n, 1)
    override def toString = numer + "/" + denom
    def + (other:Rational):Rational = {
        new Rational(
            this.numer * other.denom + other.numer * this.denom,
            this.denom * other.denom
        )
    }
    def + (i:Int):Rational = {
        new Rational(this.numer + i * this.denom, this.denom)
    }
    def - (other:Rational):Rational = {
        new Rational(
            this.numer * other.denom - other.numer * this.denom,
            this.denom * other.denom
        )
    }
    def - (i:Int):Rational = {
        new Rational(this.numer - i * this.denom, this.denom)
    }
    def * (other:Rational):Rational = {
        new Rational(this.numer * other.numer,  this.denom * other.denom)
    }
    def * (i:Int):Rational = {
        new Rational(this.numer * i,  this.denom )
    }
    def / (other:Rational):Rational = {
        new Rational(this.numer * other.denom,  this.denom * other.numer)
    }
    def / (i:Int):Rational = {
        new Rational(this.numer,  this.denom * i )
    }
    def lessThan(other:Rational) = {
        this.numer * other.denom < other.numer * this.denom
    } 
    def max(other: Rational) = {
        if(this.lessThan(other)) other else this
    }
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
    /*** 比較演算子を組み込みます ***/
    // 分母を同じ大きさにした場合の分子で大小関係を比較します
    def < (that:Rational) = this.numer * that.denom > that.numer * this.denom
    // 上の式と逆の結果を取ります
    def > (that:Rational) = that < this
    // 等号と組み合わせますよー
    def <= (that:Rational) = (this < that) || (this == that)
    // こちらも等号と組み合わせますよー
    def >= (that:Rational) = (this > that) || (this == that)
}

まあ、4つのメソッド定義ですがこれらはとてもよく使う(一般性が高い)メソッドなのでScalaでは同様の処理をOrderedトレイトを使って提供しておりますデス

Orderedトレイトを使ってみようー

せっかく用意してくれているのでOrderdトレイトを使ってみます、Orderedトレイトを利用して比較演算を行うようにRationalクラスを描き直してみます。ちなみにOrderedトレイトをミックスインするためには型パラメーターが必要なのでRatinal型を定義してやります(型パラメーターについては19章で)

// 型パラメーター指定で
class Rational(n:Int, d:Int) extends Ordered[Rational]{
  // 中略
  /*** 比較演算子使う用の定義(Ordered トレイト使う準備) ***/
  def compare(that:Rational) = 
    (this.numer * that.denom) - (that.numer * this.denom)
}

Orderedトレイとはcompareの結果を利用して<, >, <=, >=の比較結果を提供しマス。実際にはcompareの値が正、負、0のどれかになるか?という結果をもとに比較演算子の結果を求めるみたいデスネ(正なら<と<=がtrue、>と>=がfalseみたいに)

実際に演算してみる

んじゃ、確認の意味も込めて実際に演算してみますねー

// 有理数を取り出しますー
scala> var r1 = new Rational(1,3)
r1: Rational = 1/3
scala> var r2 = new Rational(1,4)
r2: Rational = 1/4

// 比較演算しますよー
scala> r1 < r2
res8: Boolean = false
scala> r1 > r2
res9: Boolean = true
scala> r1 >= r2
res10: Boolean = true
scala> r1 <= r2
res11: Boolean = false

出来ましたね(`・ω・´)

Orderedトレイトの注意点

Orderedトレイトは便利なのだけども、型イレージャー(なんか15章で出てくるみたい「型消去type erasure」って何?)のせいで型チェックができないのでequalsメソッドは定義できないみたいデス。なのでequalsメソッドは自前で用意しましょう
とのことデス。詳しくは15章、28章でー

いじょー

次はトレイトの積み重ね?とか多重継承関係の話ですかねー、トレイト結構楽しいので頑張りますよ(`・ω・´)