読者です 読者をやめる 読者になる 読者になる

無限大な夢のあと

テニスとアニメが大好きな厨二病SEのブログ

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その5(トレイト)

関数型のパラダイムを学んで業務に活かそうということで、以下のドワンゴオリジナルの新卒エンジニア向けの研修資料でScalaを本格的に勉強してみることにした。
dwango.github.io
※Java8でごにょごにょしないのは、Scalaを趣味でやりたいという意図があるだけです。

どうせなら、Scalaの言語仕様からガッツリ学んで自分のものにしたい。
また、可能な限りGolangとの比較も入れていきます。

学んだことをとにかく走り書きしていきます。
【トレイト】
・トレイトの基本
気になった部分を以下に記載

Scalaのトレイトはクラスに比べて以下のような特徴があります。

・複数のトレイトを1つのクラスやトレイトにミックスインできる
・直接インスタンス化できない
・クラスパラメータ(コンストラクタの引数)を取ることができない
以下、それぞれの特徴の紹介をしていきます。

トレイトの必要性については、後述してあるかもしれないが、以下の書籍ではこう記載してある。

クラスには 2 つの相反する役割があります。
1 つ目は「インスタンスを作るためのもの」という役割で、このためには「完結した、必要なものを全部持った、大きなクラス」である必要があります。
2 つ目は「再利用の単位」という役割で、このためには「機能ごとの、余計な物を持っていない、小さな クラス」である必要があります。
クラスが「インスタンスを作るためのもの」として使われているときには、 再利用の単位としては大きすぎるのです。
それならば、再利用の単位とい う役割に特化した、もっと小さい構造(トレイト=メソッドの束)を作ればよいのではないか?──これがトレイトの考え方です。
再利用の単位をクラスと別に作るという点では、Ruby のモジュールに似ていますね。

Rubyのモジュールとの違いは、Ruby1.9.3では、後からincludeしたモジュールはで既存のメソッドが上書きされる中で、トレイトは順番を変えても挙動が変わらないうえに、名前衝突が起こった際には明示的にエラーが発生します。

・複数のトレイトを1つのクラスやトレイトにミックスインできる

Scalaのトレイトはクラスとは違い、複数のトレイトを1つのクラスやトレイトにミックスインすることができます。

trait TraitA

trait TraitB

class ClassA

class ClassB

// コンパイルできる
class ClassC extends ClassA with TraitA with TraitB

scala> // コンパイルエラー!
     | class ClassD extends ClassA with ClassB
<console>:15: error: class ClassB needs to be a trait to be mixed in
       class ClassD extends ClassA with ClassB

上記の例ではClassAとTraitAとTraitBを継承したClassCを作ることはできますがClassAとClassBを継承したClassDは作ることができません。「class ClassB needs to be a trait to be mixed in」というエラーメッセージが出ますが、これは「ClassBをミックスインさせるためにはトレイトにする必要がある」という意味です。複数のクラスを継承させたい場合はクラスをトレイトにしましょう。

うん、ここは問題なし。
クラスを多重継承しようとした時に、コンパイルエラーが出るということで。

・直接インスタンス化できない
Scalaのトレイトはクラスと違い、直接インスタンス化できません。

scala> trait TraitA
defined trait TraitA
scala> object ObjectA {
     |   // コンパイルエラー!
     |   val a = new TraitA
     | }
<console>:15: error: trait TraitA is abstract; cannot be instantiated
         val a = new TraitA

この制限は回避する方法がいくつかあります。1つはインスタンス化できるようにトレイトを継承したクラスを作ることです。もう1つはトレイトに実装を与えてインスタンス化する方法です。

trait TraitA

class ClassA extends TraitA

object ObjectA {
  // クラスにすればインスタンス化できる
  val a = new ClassA

  // 実装を与えてもインスタンス化できる
  val a2 = new TraitA {}
}

このように実際使う上では、あまり問題にならない制限でしょう。

うん、ここは、実装を与えてもインスタンス化できるというところが、まだピンとこない。
このコード上では、実装を与えているということになるのか??
{}で実装を与えているという解釈で良いのかな。
空メソッド?

・クラスパラメータ(コンストラクタの引数)を取ることができない
Scalaのトレイトはクラスと違いパラメータ(コンストラクタの引数)を取ることができないという制限があります1。

// 正しいプログラム
class ClassA(name: String) {
  def printName() = println(name)
}
scala> // コンパイルエラー!
     | trait TraitA(name: String)
<console>:3: error: traits or objects may not have parameters
trait TraitA(name: String)

これもあまり問題になることはありません。トレイトに抽象メンバーを持たせることで値を渡すことができます。インスタンス化できない問題のときと同じようにクラスに継承させたり、インスタンス化のときに抽象メンバーを実装をすることでトレイトに値を渡すことができます。

trait TraitA {
  val name: String
  def printName(): Unit = println(name)
}

// クラスにして name を上書きする
class ClassA(val name: String) extends TraitA

object ObjectA {
  val a = new ClassA("dwango")

  // name を上書きするような実装を与えてもよい
  val a2 = new TraitA { val name = "kadokawa" }
}

以上のようにトレイトの制限は実用上ほとんど問題にならないようなものであり、その他の点ではクラスと同じように使うことができます。つまり実質的に多重継承と同じようなことができるわけです。そしてトレイトのミックスインはモジュラリティに大きな恩恵をもたらします。是非使いこなせるようになりましょう。

大まかにトレイトの基本的な使い方はこれで大丈夫そう。

・菱形継承問題
Scalaではoverride指定なしの場合メソッド定義の衝突はエラーになるということだけ理解できればOK。
ただ、Trait同士でメソッド定義がぶつかった時に、overrideとトレイト名の指定で呼び出せることだけは覚えておく。
上記のように、複数継承したものを全てを明示的に呼ぶ方法として、線形化(linearization)」という機能があるそう。

・線形化(linearization)

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

次に以下の例を考えてみます。先程の例との違いはTraitBとTraitCのgreetメソッド定義にoverride修飾子が付いていることです。

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("Good evening!")
}

class ClassA extends TraitB with TraitC

この場合はコンパイルエラーにはなりません。ではClassAのgreetメソッドを呼び出した場合、いったい何が表示されるのでしょうか?実際に実行してみましょう。

scala> (new ClassA).greet()
Good evening!

ClassAのgreetメソッドの呼び出しで、TraitCのgreetメソッドが実行されました。これはトレイトの継承順番が線形化されて、後からミックスインしたTraitCが優先されているためです。つまりトレイトのミックスインの順番を逆にするとTraitBが優先されるようになります。以下のようにミックスインの順番を変えてみます

class ClassB extends TraitC with TraitB

するとClassBのgreetメソッドの呼び出して、今度はTraitBのgreetメソッドが実行されます。

scala> (new ClassB).greet()
Good morning!

superを使うことで線形化された親トレイトを使うこともできます

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

trait TraitB extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("My name is Terebi-chan.")
  }
}

trait TraitC extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}

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

このgreetメソッドの結果もまた継承された順番で変わります。

scala> (new ClassA).greet()
Hello!
My name is Terebi-chan.
I like niconico.

scala> (new ClassB).greet()
Hello!
I like niconico.
My name is Terebi-chan.

線形化の機能によりミックスインされたすべてのトレイトの処理を簡単に呼び出せるようになりました。このような線形化によるトレイトの積み重ねの処理をScalaの用語では積み重ね可能なトレイト(Stackable Trait)と呼ぶことがあります。

この線形化がScalaの菱形継承問題に対する対処法になるわけです。

overrideしたメソッドの中でsuperを使用して、新たに定義したい内容を記述する。
このような設計にならないようにはするが、やり方は覚えておこう。

・abstract override

通常のメソッドのオーバーライドでsuperを使ってスーパークラスのメソッドを呼びだす場合、当然のことながら継承元のスーパークラスにそのメソッドの実装がなければならないわけですが、 Scalaには継承元のスーパークラスにそのメソッドの実装がない場合でもメソッドのオーバーライドが可能なabstract overrideという機能があります。

abstract overrideではないoverrideとabstract overrideを比較してみましょう。

trait TraitA {
  def greet(): Unit
}
scala> // コンパイルエラー!
     | trait TraitB extends TraitA {
     |   override def greet(): Unit = {
     |     super.greet()
     |     println("Good morning!")
     |   }
     | }
<console>:16: error: method greet in trait TraitA is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'
           super.greet()
// コンパイルできる
trait TraitC extends TraitA {
  abstract override def greet(): Unit = {
    super.greet()
    println("Good evening!")
  }
}

abstract修飾子を付けていないTraitBはコンパイルエラーになってしまいました。エラーメッセージの意味は、TraitAのgreetメソッドには実装がないのでabstract overrideを付けない場合オーバーライドが許されないということです。

オーバーライドをabstract overrideにすることでスーパークラスのメソッドの実装がない場合でもオーバーライドすることができます。この特性は抽象クラスに対しても積み重ねの処理が書けるということを意味します。

しかしabstract overrideでも1つ制約があり、ミックスインされてクラスが作られるときにはスーパークラスのメソッドが実装されてなければなりません。

trait TraitA {
  def greet(): Unit
}

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

trait TraitC extends TraitA {
  abstract override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}
scala> // コンパイルエラー!
     | class ClassA extends TraitC
<console>:15: error: class ClassA needs to be a mixin, since method greet in trait TraitC of type ()Unit is marked `abstract' and `override', but no concrete implementation could be found in a base class
       class ClassA extends TraitC
// コンパイルできる
class ClassB extends TraitB with TraitC

abstract overrideの制約のミックスインされてクラスが作られるときにはスーパークラスのメソッドが実装されてならないということだけ、覚えておこう。(当たり前といえば、当たり前だが)

・自分型

Scalaにはクラスやトレイトの中で自分自身の型にアノテーションを記述することができる機能があります。これを自分型アノテーション(self type annotations)や単に自分型(self types)などと呼びます。

trait Greeter {
  def greet(): Unit
}

trait Robot {
  self: Greeter =>

  def start(): Unit = greet()
  override final def toString = "Robot"
}

このロボット(Robot)は起動(start)するときに挨拶(greet)するようです。 Robotは直接Greeterを継承していないのにもかかわらずgreetメソッドを使えていることに注意してください。

このロボットのオブジェクトを実際に作るためにはgreetメソッドを実装したトレイトが必要になります。 REPLを使って動作を確認してみましょう。

scala> trait HelloGreeter extends Greeter {
     |   def greet(): Unit = println("Hello!")
     | }
defined trait HelloGreeter

scala> val r = new Robot with HelloGreeter
r: Robot with HelloGreeter = Robot

scala> r.start()
Hello!

自分型を使う場合は、抽象トレイトを指定し、後から実装を追加するという形になります。このように後から(もしくは外から)利用するモジュールの実装を与えることを依存性の注入(Dependency Injection)と呼ぶことがあります。自分型を使われている場合、この依存性の注入のパターンが使われていると考えてよいでしょう。

ではこの自分型によるトレイトの指定は以下のように直接継承する場合と比べてどのような違いがあるのでしょうか。

trait Greeter {
  def greet(): Unit
}

trait Robot2 extends Greeter {
  def start(): Unit = greet()
  override final def toString = "Robot2"
}

オブジェクトを生成するという点では変わりません。 Robot2も先程と同じように作成することができます。ただし、このトレイトを利用する側や、継承したトレイトやクラスにはGreeterトレイトの見え方に違いができます。

scala> val r: Robot = new Robot with HelloGreeter
r: Robot = Robot
scala> r.greet()
<console>:17: error: value greet is not a member of Robot
       r.greet()
         ^
scala> val r: Robot2 = new Robot2 with HelloGreeter
r: Robot2 = Robot2

scala> r.greet()
Hello!

継承で作られたRobot2オブジェクトではGreeterトレイトのgreetメソッドを呼び出せてしまいますが、自分型で作られたRobotオブジェクトではgreetメソッドを呼びだすことができません。

Robotが利用を宣言するためにあるGreeterのメソッドが外から呼び出せてしまうことはあまり良いことではありません。この点で自分型を使うメリットがあると言えるでしょう。逆に単に依存性を注入できればよいという場合には、この動作は煩わしく感じられるかもしれません。

テスタビリティ的には、可能な限りDIした方が良いと思うので、自分型は積極的に使っていった方が良いと思う。

もう1つ自分型の特徴としては型の循環参照を許す点です。

自分型を使う場合は以下のようなトレイトの相互参照を許しますが、

trait Robot {
  self: Greeter =>

  def name: String

  def start(): Unit = greet()
}

// コンパイルできる
trait Greeter {
  self: Robot =>

  def greet(): Unit = println(s"My name is $name")
}

これを先ほどのように継承に置き換えることではできません。

scala> trait Robot extends Greeter {
     |   def name: String
     | 
     |   def start(): Unit = greet()
     | }
<console>:16: error: illegal inheritance;
 self-type Robot does not conform to Greeter's selftype Greeter with Robot
       trait Robot extends Greeter {
                           ^

scala> // コンパイルエラー
     | trait Greeter extends Robot {
     |   def greet(): Unit = println(s"My name is $name")
     | }
<console>:15: error: illegal inheritance;
 self-type Greeter does not conform to Robot's selftype Robot with Greeter
       trait Greeter extends Robot {

しかし、このように循環するような型構成を有効に使うのは難しいかもしれません。

依存性の注入を使う場合、継承を使うか、自分型を使うかというのは若干悩ましい問題かもしれません。機能的には継承があればよいと言えますが、上記のような可視性の問題がありますし、自分型を使うことで依存性の注入を利用しているとわかりやすくなる効果もあります。利用する場合はチームで相談するとよいかもしれません。

型の循環参照はやめておいたほうが良いという印象。
ここはこういうことができるよという紹介かな。

・落とし穴:トレイトの初期化順序

Scalaのトレイトのvalの初期化順序はトレイトを使う上で大きな落とし穴になります。以下のような例を考えてみましょう。トレイトAで変数fooを宣言し、トレイトBがfooを使って変数barを作成し、クラスCでfooに値を代入してからbarを使っています。

trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

REPLでクラスCのprintBarメソッドを呼び出してみましょう。

scala> (new C).printBar()
nullWorld

nullWorldと表示されてしまいました。クラスCでfooに代入した値が反映されていないようです。どうしてこのようなことが起きるかというと、Scalaのクラスおよびトレイトはスーパークラスから順番に初期化されるからです。この例で言えば、クラスCはトレイトBを継承し、トレイトBはトレイトAを継承しています。つまり初期化はトレイトAが一番先におこなわれ、変数fooが宣言され、中身は何も代入されていないので、nullになります。次にトレイトBで変数barが宣言されnullであるfooと"World"という文字列から"nullWorld"という文字列が作られ、変数barに代入されます。先ほど表示された文字列はこれになります。

このような簡単な例なら気づきやすいのですが、似たような形の大規模な例もあります。先ほど自分型で紹介した「依存性の注入」は、上位のトレイトで宣言したものを、中間のトレイトで使い、最終的にインスタンス化するときにミックスインするという手法です。ここでもうっかりすると同じような罠を踏んでしまいます。 Scala上級者でもやってしまうのがvalの初期化順の罠なのです。

これは絶対どこかでやってしまいそう。
Scalaスーパークラスから順番に初期化されることは覚えておこう。

・トレイトのvalの初期化順序の回避方法

では、この罠はどうやれば回避できるのでしょうか。上記の例で言えば、使う前にちゃんとfooが初期化されるように、barの初期化を遅延させることです。処理を遅延させるにはlazy valかdefを使います。

具体的なコードを見てみましょう。

trait A {
  val foo: String
}

trait B extends A {
  lazy val bar = foo + "World" // もしくは def bar でもよい
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

先ほどのnullWorldが表示されてしまった例と違い、barの初期化にlazy valが使われるようになりました。これによりbarの初期化が実際に使われるまで遅延されることになります。その間にクラスCでfooが初期化されることにより、初期化前のfooが使われることがなくなるわけです。

今度はクラスCのprintBarメソッドを呼び出してもちゃんとHelloWorldと表示されます。

scala> (new C).printBar()
HelloWorld

lazy valはvalに比べて若干処理が重く、複雑な呼び出しでデッドロックが発生する場合があります。 valのかわりにdefを使うと毎回値を計算してしまうという問題があります。しかし、両方とも大きな問題にならない場合が多いので、特にvalの値を使ってvalの値を作り出すような場合はlazy valかdefを使うことを検討しましょう。

トレイトのvalの初期化順序を回避するもう1つの方法としては事前定義(Early Definitions)を使う方法もあります。事前定義というのはフィールドの初期化をスーパークラスより先におこなう方法です。

trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World" // valのままでよい
}

class C extends {
  val foo = "Hello" // スーパークラスの初期化の前に呼び出される
} with B {
  def printBar(): Unit = println(bar)
}

上記のCのprintBarを呼び出しても正しくHelloWorldと表示されます。

この事前定義は利用側からの回避方法ですが、この例の場合はトレイトBのほうに問題がある(普通に使うと初期化の問題が発生してしまう)ので、トレイトBのほうを修正したほうがいいかもしれません。

トレイトの初期化問題は継承されるトレイト側で解決したほうが良いことが多いので、この事前定義の機能は実際のコードではあまり見ることはないかもしれません。

トレイトの初期化問題、lazy valあるいはdefで解決するとのこと。
事前定義などと裏技もあるのか。
デッドロック問題も頭に留めておこう。


ちなみに、Golangはクラスではなく、すべてInterfaceや構造体で継承のようなことをやるので、今回は全体的に記述なし。


Programming in Scala: Updated for Scala 2.12

Programming in Scala: Updated for Scala 2.12

Scalaパズル 36の罠から学ぶベストプラクティス

Scalaパズル 36の罠から学ぶベストプラクティス

SCALAプログラミング入門

SCALAプログラミング入門