ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Scalaの勉強 - トレイト(2)

スポンサーリンク

ちょっと間が空いたけど、再びScalaの勉強に取り組んでいく。

前回のScalaネタ最後に

まだこの章は続くのだけど、今日はここまで。

www.tech-diningyo.info

と書いた。なので今日はトレイトの章の残りを見ていく。

トレイトの様々な機能

菱型継承問題

Scalaではトレイトを使うことで、実質的には多重継承を行うことが出来る。そのときに問題になるのが菱型継承問題

資料には以下のようにシンプルな例が示されている。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitB
class ClassC extends TraitC

object DiamondProblem {
  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()
    val o_b = new ClassB()
    val o_c = new ClassC()
  }
}

上記の例では、TraitAを継承したTraitB/TraitCでメソッドgreetを実装している。このTraitB/TraitCを同時に継承したClassAを作ると以下のようにエラーが発生する。TraitB/Cでメソッドgreetの実装が存在しており、この2つのTraitを継承してしまうとどちらのgreetを使うのかが一意に決まらなくなることが原因となる。これが菱型継承問題だ。

$ scala 20180929_01_trait_2_inheritance_diamond_problem.scala 
20180929_01_trait_2_inheritance_diamond_problem.scala:13: error: class ClassA inherits conflicting members:
  method greet in trait TraitB of type ()Unit  and
  method greet in trait TraitC of type ()Unit
(Note: this can be resolved by declaring an override in class ClassA.)
class ClassA extends TraitB with TraitC
      ^
one error found

overrideによるメソッドの上書き

上記のNoteにあるようにScalaではClassAoverride指定を行うことでこの問題を回避できるらしい。ということでやってみる。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC
{
  override def greet(): Unit = println("How are you?")
}

object DiamondProblem {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()

    o_a.greet()
  }
}

見たとおりだが、ClassAの実装にてoverride def greet()として実装を上書きして実行してみると、上記のようにClassAで実装した動作となる。

$ scala 20180929_02_trait_2_inheritance_diamond_problem_override.scala 
How are you?

上記の例では、overrideしたメソッドで新規の処理を定義したが、TraitB or TraitCの処理を使用したい場合もあるはずでその時のためにsuper[]を使ってどちらのTraitの処理を使うかを明示的に指定することが出来る。実際にコードを書き換えてTraitBの処理を呼び出してみる。このsuperを使う感じはpythonと一緒だな。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC
{
  override def greet(): Unit = super[TraitB].greet()
}

object DiamondProblem {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()

    o_a.greet()
  }
}

実際に実行してみるとエラーがもなく実行することが出来て、結果もsuperで指定したTraitBの処理が実行される。

$ scala 20180929_03_trait_2_inheritance_diamond_problem_super.scala 
Good Morning

以下のようにTraitB/TraitCの物を両方呼び出すことも可能

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC
{
  override def greet(): Unit = {
    super[TraitB].greet()
    super[TraitC].greet()
  }

}

object DiamondProblem {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()

    o_a.greet()
  }
}

結果は以下の通り。

$ scala 20180929_04_trait_2_inheritance_diamond_problem_super2.scala 
Good Morning
Goog Evening

線形化(linearization)

前項のようにsuperを使うことで明示的に継承元のメソッドを呼び出すことが可能だ。だが、継承関係が複雑になった場合、それらを全て把握して明示的に処理をすることが大変になってくる。

Scalaでは線形化という機能を使うことでそれを解決することが出来るらしい。

資料によると線形化とは

Scalaのトレイトの線形化機能とは、トレイトがミックスインされた順番をトレイトの継承順番と見做すことです。

とのことだ。

よくわからんので例を見ていく。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  override def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  override def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC

object Linearization {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()

    o_a.greet()
  }
}

上記は前項で多重継承でエラーとなる例に少しだけ手を加えたものだ。変更点は一つで

  • 継承して作ったTraitB/TraitCgreetメソッドにoverrideをつけた

だけになる。

これを実行してみよう。

$ scala 20180929_05_trait_2_linearization1.scala 
Goog Evening

今度はエラーにならずに実行できた。結果としてはTraitCgreetメソッドが表示されている。これだけだと、あんまりメリットがわからないので、上のコードをまた少しいじってみよう。

trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  override def greet(): Unit = println("Good Morning")
}

trait TraitC extends TraitA {
  override def greet(): Unit = println("Goog Evening")
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB

object Linearization {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()
    val o_b = new ClassB()

    o_a.greet()
    o_b.greet()
  }
}

追加したのはClassBの定義のみでClassAとの違いはextendsTraitCを指定してwithTraitBを指定したことのみであるが、このコードを実行すると以下のようになる。

$ scala 20180929_06_trait_2_linearization2.scala 
Goog Evening
Good Morning

見てのとおりだが、継承の順番によって実行されるgreetメソッドが異なっている。説明としては

これはトレイトの継承順番が線形化されて、後からミックスインしたTraitCが優先されているためです。

とのこと。

指定する順番によって継承順が一意に決まる(=線形化する)とのことであとからミックスインしたTraitCが優先されることになる。

superを使うことで線形化された親トレイトを使うことも可能。

trait TraitA {
  def greet(): Unit = println("Hello")
}

trait TraitB extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("Good Morning")
  }
}

trait TraitC extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("Goog Evening")
  }
}

trait TraitD extends TraitA {
  override def greet(): Unit = {
    super[TraitA].greet()
    println("Goog Evening")
  }
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB
class ClassC extends TraitC with TraitD

object Linearization {

  def main(args: Array[String]): Unit = {
    val o_a = new ClassA()
    val o_b = new ClassB()
    val o_c = new ClassC()

    println("1. class ClassA extends TraitB with TraitC")
    o_a.greet()

    println("2. class ClassB extends TraitC with TraitB")
    o_b.greet()

    println("3. class ClassB extends TraitC with TraitD")
    o_c.greet()
  }
}

「親トレイトを使うことも可能」( ・ิω・ิ)と資料の内容をそのまんまパクってみたけど、どういうことか理解できなかったので資料のサンプルの他に追加してTraitDとそれを使ったClassCを作ってみたものを実行すると以下のようになった。

$ scala 20180929_07_trait_2_linearization3.scala 
1. class ClassA extends TraitB with TraitC
Hello
Good Morning
Goog Evening
2. class ClassB extends TraitC with TraitB
Hello
Goog Evening
Good Morning
3. class ClassB extends TraitC with TraitD
Hello
Goog Evening

結果をまとめると以下の感じだろーか。

  1. TraitBTraitCの場合
  2. TraitCが優先されるのでまずはTraitC.greet()が呼ばれる
  3. TraitC.greet()内でsuper.greet()が処理されるが、この場合のsuperTraitBになるのでTraitB.greet()が呼ばれる
  4. TraitB.greet()内部でsuper.greet()が呼ばれるのでTraitA.greet()が呼ばれる
  5. なので実際のprintln()の呼ばれる順番はTraitATraitBTraitCの順になり上記の結果の順に結果が出力される
  6. TraitCTraitBの場合
    1. 1.のTtaitB/TraitCの関係が逆になるので、"Good Morning"と"Good Evening"の順がひっくり返る
  7. TraitDTraitCの場合
    1. TraitDの内部で読んでいるのはsuper[TraitA].greet()になので、TraitB.greet()が呼ばれないため"Good Morning"が出力されない。

このような処理を指して積み重ね可能なTraitと呼ぶことがあるとのこと↓

このような線形化によるトレイトの積み重ねの処理をScalaの用語では積み重ね可能なトレイト(Stackable Trait)と呼ぶことがあります。

思いの外長くなったので、トレイトはもう一回使って見ていくことにして今日はここまで。