M.Hiroi's Home Page

お気楽 Scala プログラミング入門

トレイト


Copyright (C) 2014-2024 Makoto Hiroi
All rights reserved.

はじめに

前回は継承について簡単に説明しました。今回は「トレイト (trait)」について取り上げます。

●トレイトとは?

Scala の「トレイト」は、Java の「インターフェース」と比較するならば、実装を持つことが可能なインターフェース、ということができます。Java のインターフェースはメソッドの仕様 (抽象メソッド) だけを定義した抽象クラスの一種ですが、「多重継承」できるところが他のクラスと異なるところです。

Scala のトレイトは、基本的には Java のインターフェースと同じですが、大きな違いは実装を持たせることができるところです。メソッドだけではなくフィールド変数も持たせることができます。Scala のトレイトは多重継承が簡単にできるように機能を制限した「クラス」と考えることができます。

クラスを多重継承するとき複雑な問題を引き起こす場合がありますが、その問題点について把握しておくと、トレイトの理解が深まると思います。そこで、まず最初に多重継承の問題点について説明します。

●多重継承とその問題点

一般的なオブジェクト指向の場合、継承によって引き継がれる性質は定義されたデータ (インスタンス変数など) やメソッドになります。これを「実装の継承」と呼びます。また、インスタンス変数を継承することを特別に「属性の継承」と呼ぶ場合があります。

多重継承を行う場合、異なる性質や機能を持つクラスを継承することがあります。たとえば、クラス Foo にはメソッド method_a があり、クラス Bar にはメソッド method_b があるとしましょう。この 2 つのメソッドはまったく異なる働きをするとします。ここで、メソッド method_a はインスタンス変数 x を使っていて、method_b も x を使っていると、多重継承で問題が発生します。

一般的な多重継承で、クラス Foo と Bar を継承してクラス Baz を作成した場合、クラス Baz のインスタンスに変数 x は一つしか存在しません。メソッド method_a と method_b は一つしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありませんね。

また、多重継承ではインスタンス変数だけではなく、メソッド名が衝突する場合もあります。このように、多重継承では名前の衝突が発生する危険性があるのです。それから、多重継承にはもう一つ問題点があります。それはクラスの階層構造が複雑になることです。

単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単に決めることができますが、グラフになると優先順位を決めるためのアルゴリズムは複雑になり、それを理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要になるのです。

ちなみにC++の場合、多重継承したクラスに同名のメソッドがある場合、どちらを呼び出すのか明確に指定しないとコンパイルでエラーとなります。またC++はメンバ変数も継承されるため、変数名の衝突も発生します。この場合も、どちらの変数を使用するのか明確に指定しないとコンパイルエラーとなります。

このほかにも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++ではいろいろな機能が用意されています。ところが、それらの機能がC++をいっそう複雑な言語にしていると M.Hiroi には思えてなりません。C++はコンパイラ型の言語で、なによりも効率を重視するため、複雑な言語仕様になるのは避けられないのかもしれません。

●Mix-in

これらの問題を回避するため、属性を継承するスーパークラスは一つだけに限定して、あとのスーパークラスは実装だけを継承するという方法があります。これを Mix-in といいます。

具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。一つのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。

なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。この Mix-in という方法を言語仕様に取り込んでいるのが Ruby です。Mix-in を下図に示します。

                A
              /
            B
 Mixin A  /  \    Mixin B
    \  /      \  /
      C          D

      図 : Mix-in

クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。

多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。

Scala のクラスは単一継承ですが、トレイトは多重継承ができるので、Scala でも Mix-in を使うことができます。また、トレイトは型として使用することもできます。つまり、クラスをトレイトの型に変換することで、ポリモーフィズムを有効に活用することができるわけです。

●トレイトの使い方

それでは、具体的にトレイトの使い方を説明しましょう。トレイトの定義は trait 文で行います。trait の構文を示します。

trait 名前 extends クラス or トレイト [with トレイト] {

  ...

}

trait の後ろに名前を指定します。トレイトは extends で他のクラスを継承することができます。継承するクラスがない場合は extends にトレイトを指定することができます。トレイトは with を使って指定します。多重継承する場合は、"with トレイト" を複数記述します。

そして、ブロックの中でフィールド変数やメソッドなどを宣言します。実装を定義することも可能です。ただし、コンストラクタ引数を持つことはできません。補助コンストラクタも定義することはできません。また、トレイト単独でインスタンスを生成することはできません。ご注意くださいませ。

クラスでトレイトを継承する場合も extends や with を使います。指定方法は trait と同じです。with を使って複数のトレイトを継承することができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : トレイトの使い方

class Foo(val a: Int = 1, val b: Int = 2) {
  def foo(): Unit = println("Foo")
  def method(): Unit = println("Foo method")
}

trait Bar1 {
  def bar1(): Unit = println("Bar1")
  def method(): Unit = println("Bar1 method")
}

trait Bar2 {
  def bar2(): Unit = println("Bar2")
  def method(): Unit = println("Bar2 method")
}

class Baz extends Foo with Bar1 with Bar2 {
  override def method(): Unit = super.method()
}

object sample0904 {
  def main(args: Array[String]): Unit = {
    val a = new Baz
    println(a.a)
    println(a.b)
    a.foo()
    a.bar1()
    a.bar2()
    a.method()
  }
}

クラス Foo にはフィールド変数 a, b とメソッド method、トレイト Bar1, bar2 にはメソッド method が定義されています。Foo と Bar1, Bar2 を継承するクラス Baz を定義する場合、同名のメソッド method があるので、method を Baz でオーバーライドしてください。オーバーライドしないと、どのメソッドを呼び出したらよいかコンパイラが決定できずにエラーとなります。

実行結果は次のようになります。

$ scalac sample0904.scala
$ scala sample0904
1
2
Foo
Bar1
Bar2
Bar2 method

継承したメソッド foo, bar1, bar2 を呼び出すことができるのは当然ですが、3 つのクラスにある method は、どのクラスのメソッドが呼び出されるのでしょうか。表示された Bar2 method から、トレイト Bar2 のメソッドが呼び出されたことがわかります。

多重継承でメソッドを探索する場合、簡単に言うと with の右側から順番に行われ、最後に extends で指定したクラスを探します。そして、最初に見つかったメソッドを実行します。したがって、with でトレイトの順番を逆にすると、今度は Bar1 の method が実行されます。

なお、トレイト Bar1 と Bar2 にスーパークラスが設定されている場合、メソッドの探索はもっと複雑になります。Scala の場合、メソッドの優先順位は「線形化」という方法で決定します。「線形化」については本稿の範囲を超えるので説明は割愛します。興味のある方は調べてみてください。

特定のスーパークラスのメソッドを呼び出したい場合は、super の後ろに [クラス名] を指定します。たとえば、Baz で Foo の method を呼び出したい場合は次のように指定します。

override def method(): Unit = super[Foo].method

これで Foo の method を呼び出すことができます。

●図形のクラス

それでは簡単な例として、前回作成した図形のプログラムをトレイトを使って書き直してみましょう。次のリストを見てください。

リスト : 図形のクラス (トレイト版)

trait Figure {
  def kindOf: String
  def area: Double
  def print(): Unit = println(kindOf + ": area = " + area)
}

class Triangle(val altiude: Double, val base: Double) extends Figure {
  def kindOf: String = "Triangle"
  def area: Double = altiude * base / 2
}

class Rectangle(val width: Double, val height: Double) extends Figure {
  def kindOf: String = "Rectangle"
  def area: Double = width * height
}

class Circle(val radius: Double) extends Figure {
  def kindOf: String = "Circle"
  def area: Double = radius * radius * Math.PI
}

object sample0905 {
  def sumOfArea(xs: List[Figure]): Double = {
    var sum = 0.0
    for (x <- xs) sum += x.area
    sum
  }
  
  def main(args: Array[String]): Unit = {
    val a = new Triangle(2, 2)
    val b = new Rectangle(2, 2)
    val c = new Circle(2)
    a.print()
    b.print()
    c.print()
    println(sumOfArea(List(a, b, c)))
  }
}
$ scalac sample0905.scala
$ scala sample0905
Triangle: area = 2.0
Rectangle: area = 4.0
Circle: area = 12.566370614359172
18.566370614359172

最初に図形を操作するトレイト Figure を定義します。この中でメソッド kindOf, area, を宣言し、print の実装を定義します。次に図形を表すクラス Triangle, Rectangle, Circle を定義します。ここで extends で Figure を継承します。そして、各クラスで Figure のメソッドを実装します。

クラスは継承したトレイトの型にアップキャストすることができます。どのクラスも Figure を継承しているので型 List[Figure] のリストに格納することができます。

面積の合計を求める関数 sumOfArea は引数に Figure 型のリストを受け取ります。そして、Figure に定義されているメソッドを使ってインスタンスを操作することができます。sumOfArea はメソッド area を呼び出していますが、ポリモーフィズムの働きにより各クラスのメソッド area が呼び出されるので図形の面積を正しく求めることができます。

●Ord トレイト

もうひとつ簡単な例題として、値の大小関係を比較するための演算子を定義するトレイト Ord を作ってみましょう。次のリストを見てください。

リスト : Ord トレイト

trait Ord {
  def <(that: Any): Boolean
  def <=(that: Any): Boolean = this < that || this == that
  def >(that: Any): Boolean = !(this < that) && this != that
  def >=(that: Any): Boolean = !(this < that)
}

Scala の演算子はメソッドとして定義されています。たとえば、1 + 2 は (1).+(2) と呼び出すこともできます。1. は浮動小数点数として扱われるので、1 はカッコで囲んでください。== と != はメソッド equals をオーバーライドすれば使用することができます。演算子 < を抽象メソッドとすると、演算子 <=, >, >= は <, ==, != で定義することができます。

それでは図形の抽象クラス Figure に Ord を実装してみましょう。値の大小関係は面積で比較します。次のリストを見てください。

リスト : Ord の使用例

trait Ord {
  def <(that: Any): Boolean
  def <=(that: Any): Boolean = this < that || this == that
  def >(that: Any): Boolean = !(this < that) && this != that
  def >=(that: Any): Boolean = !(this < that)
}

abstract class Figure extends Ord {
  def kindOf: String
  def area: Double
  def print(): Unit = println(kindOf + ": area = " + area)
  override def equals(other: Any): Boolean = other match {
    case that: Figure => area == that.area
    case _ => false
  }
  def <(other: Any): Boolean = other match {
    case that: Figure => area < that.area
    case _ => false
  }
}

class Triangle(val altiude: Double, val base: Double) extends Figure {
  def kindOf: String = "Triangle"
  def area: Double = altiude * base / 2
}

class Rectangle(val width: Double, val height: Double) extends Figure {
  def kindOf: String = "Rectangle"
  def area: Double = width * height
}

class Circle(val radius: Double) extends Figure {
  def kindOf: String = "Circle"
  def area: Double = radius * radius * Math.PI
}

object sample0906 {
  def insertSort(xs: List[Figure]): List[Figure] = {
    def insertElement(x: Figure, xs: List[Figure]): List[Figure] =
      xs match {
        case Nil => List(x)
        case y::ys => if (x < y) x::y::ys else y::insertElement(x, ys)
      }
    xs match {
      case Nil => Nil
      case y::ys => insertElement(y, insertSort(ys))
    }
  }
  def main(args: Array[String]): Unit = {
    val a = new Triangle(5, 5)
    val b = new Rectangle(2, 2)
    val c = new Circle(2)
    for (x <- insertSort(List(a, b, c))) x.print()
  }
}
$ scalac sample0906.scala
$ scala sample0906
Rectangle: area = 4.0
Triangle: area = 12.5
Circle: area = 12.566370614359172

抽象クラス Figure でトレイト Ord を継承します。このクラスでメソッド equals をオーバーライドして、メソッド < を実装します。どちらのメソッドも match 式で引数 other の型が Figure であることを確認し、メソッド area を呼び出して大小関係をチェックします。

関数 insertSort は拙作のページ「パターンマッチング」で作成した単純挿入ソートの型を Int から Figure に変えたものです。これで図形を格納したリスト (List[Figure]) をソートすることができます。

●クラスからトレイトへのキャスト

Java の場合、どんなクラスでも明示的にキャストすればインターフェースへ型変換することができますが、Scala はそうではありません。isInstanceOf[トレイト] の返り値が true であれば、asInstancOf[トレイト] でキャストすることができますが、返り値が false の場合はキャストすることはできません。

簡単な例を示しましょう。

scala> trait foo { def bar(): Unit }
// defined trait foo

scala> class Baz1 extends foo { def bar(): Unit = println("oops!") }
// defined class Baz1

scala> class Baz2
// defined class Baz2

scala> val a = new Baz1
val a: Baz1 = Baz1@231b35fb

scala> val b = new Baz2
val b: Baz2 = Baz2@72fb989b

scala> val c: foo = a
val c: foo = Baz1@231b35fb

scala> b.isInstanceOf[foo]
val res0: Boolean = false

scala> val d: foo = b.asInstanceOf[foo]
java.lang.ClassCastException: class rs$line$3$Baz2 cannot be cast to class 
rs$line$1$foo (rs$line$3$Baz2 and rs$line$1$foo are in unnamed module of 
loader dotty.tools.repl.AbstractFileClassLoader @6719a5b8)
  ... 33 elided

トレイト foo を定義します。クラス Baz1 は foo を継承し、メソッド bar を定義します。クラス Baz2 は foo を継承せず、メソッド bar も定義していません。次に、Baz1 と Baz2 のインスタンスを生成します。Baz1 は foo を継承しているので、キャストしなくても foo に型変換することができます。Baz2 は foo を継承していないので、b.isInstanceOf[foo] の返り値は false になり、b.asInstanceOf[foo] でキャストしても失敗します。


初版 2014 年 8 月 16 日
改訂 2024 年 12 月 20 日