ちょっと間が空いたけど、再びScalaの勉強に取り組んでいく。
前回のScalaネタ最後に
まだこの章は続くのだけど、今日はここまで。
と書いた。なので今日はトレイトの章の残りを見ていく。
トレイトの様々な機能
菱型継承問題
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ではClassA
でoverride
指定を行うことでこの問題を回避できるらしい。ということでやってみる。
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
/TraitC
のgreet
メソッドにoverride
をつけた
だけになる。
これを実行してみよう。
$ scala 20180929_05_trait_2_linearization1.scala Goog Evening
今度はエラーにならずに実行できた。結果としてはTraitC
のgreet
メソッドが表示されている。これだけだと、あんまりメリットがわからないので、上のコードをまた少しいじってみよう。
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
との違いはextends
にTraitC
を指定してwith
にTraitB
を指定したことのみであるが、このコードを実行すると以下のようになる。
$ 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
結果をまとめると以下の感じだろーか。
TraitB
→TraitC
の場合TraitC
が優先されるのでまずはTraitC.greet()
が呼ばれるTraitC.greet()
内でsuper.greet()
が処理されるが、この場合のsuper
はTraitB
になるのでTraitB.greet()
が呼ばれるTraitB.greet()
内部でsuper.greet()
が呼ばれるのでTraitA.greet()
が呼ばれる- なので実際の
println()
の呼ばれる順番はTraitA
→TraitB
→TraitC
の順になり上記の結果の順に結果が出力される TraitC
→TraitB
の場合- 1.の
TtaitB
/TraitC
の関係が逆になるので、"Good Morning"と"Good Evening"の順がひっくり返る
- 1.の
TraitD
→TraitC
の場合TraitD
の内部で読んでいるのはsuper[TraitA].greet()
になので、TraitB.greet()
が呼ばれないため"Good Morning"が出力されない。
このような処理を指して積み重ね可能なTraitと呼ぶことがあるとのこと↓
このような線形化によるトレイトの積み重ねの処理をScalaの用語では積み重ね可能なトレイト(Stackable Trait)と呼ぶことがあります。
思いの外長くなったので、トレイトはもう一回使って見ていくことにして今日はここまで。