トレイトの継承の線形化

昨日と同じように、菱形継承問題を考える。

scala> trait TraitA {
     |     def greet(): Unit
     | }
defined trait TraitA

scala> trait TraitB extends TraitA {
     |     override def greet(): Unit = println("Good morning!")
     | }
defined trait TraitB

scala> trait TraitC extends TraitA {
     |     override def greet(): Unit = println("Good evening!")
     | }
defined trait TraitC

昨日と違うのは、TraitB と TraitC で greet メソッドを実装するときに override キーワードをつけているところだ。この場合、つぎのように、単純に2つを継承したクラスを作ってもエラーにならない。

scala> class ClassA extends TraitB with TraitC
defined class ClassA

ここで、ClassA の greet メソッドを呼び出すと何を出力するか。試してみよう。

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

Good evening! と表示された。ということは TraitC の greet メソッドが呼ばれたってことだ。

つぎに、TraitB と TraitC の継承順を入れ替えた ClassB を考えよう。

scala> class ClassB extends TraitC with TraitB
defined class ClassB

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

今度は TraitB の greet メソッドが呼び出されている。

Scala では、こういう形で複数のトレイトを継承(ミックスイン)した場合、あとからミックスインしたトレイトが優先される。この機能をトレイトの線形化と呼ぶ。

さて、ここからがちょっとよくわからない。メソッドの中で super を使うことで親トレイトのメソッドを呼び出すことができる。定義はこんなふうだ。

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

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

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

scala> class ClassA extends TraitB with TraitC
defined class ClassA

scala> class ClassB extends TraitC with TraitB
defined class ClassB

ClassA の greet メソッドを呼んでみよう。

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

あとからミックスインされた TraitC の greet メソッドが呼ばれるはずだから I like niconico. と出力されるのはわかる。super を使って親トレイトの greet メソッドも呼んでいるんだから Hello! と出力されるのもわかる。だけどなんで My name is Terebi-chan. と出力されるんだ?これは TraitB のメソッドの出力のはずだろう?

TraitB と TraitC の間には親子関係がないはずなのに、まるで TraitB が TraitC の親トレイトであるかのようになっている。線形化ってそういうものなのか?

ClassC では出力の順番が変わる。

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

線形化の順番が違うので出力の順番も違うってことだろう。とは思うけど、なんとなく納得がいかない。

菱形継承問題

Scala のクラスは単一継承だけど、トレイトは複数継承できるので菱形継承問題が起きる。菱形継承問題というのは次のようなものだ。

greet メソッドを定義した TraitA と、それを継承してそれぞれ別の greet メソッドを実装した TraitB と TraitC があるとする。

scala> trait TraitA {
     |     def greet(): Unit
     | }
defined trait TraitA

scala> trait TraitB extends TraitA {
     |     def greet(): Unit = println("Good morning!")
     | }
defined trait TraitB

scala> trait TraitC extends TraitA {
     |     def greet(): Unit = println("Good evening!")
     | }
defined trait TraitC

ここで TraitB と TraitC の両方を継承したクラスを考えよう。すると、継承の系統図が菱形になる。で、どこが問題かというと、greet メソッドの実装が衝突している、ということだ。これを菱形継承問題という。

Scala で単純にこういう継承を作るとエラーになる。

scala> class ClassA extends TraitB with TraitC
<console>: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
             ^

TraitB と TraitC を継承した ClassA で greet メソッドが衝突していると怒られている。

これを解決するには ClassA で greet メソッドをオーバーライドしてやればいい。

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

継承元の TraitB の greet メソッドを呼び出したいときには次のように super キーワードを使う。[ ] の中に呼び出したいほうのトレイト名を書けばいい。

scala> class ClassB extends TraitB with TraitC {
     |     override def greet(): Unit = super[TraitB].greet()
     | }
defined class ClassB

呼び出した結果はそれぞれ次のようになる。

scala> (new ClassA).greet()
How are you?
scala> (new ClassB).greet()
Good morning!

トレイト

聴きなれない単語が出てきた。トレイトというのは、Scala のオブジェクト指向プログラミングにおけるモジュール化の中心的な概念らしい。トレイトの特徴は次の3つだ。

  • 1つのクラスやトレイトに複数のトレイトを継承(ミックスイン)できる
  • 直接インスタンス化できない
  • クラスパラメータ(コンストラクタの引数)をとることができない

とりあえずは Ruby のモジュールのようなものだと理解した。

トレイトの定義

定義構文はシングルトンオブジェクトの定義に似ていて、object キーワードの代わりに trait キーワードを使う。

scala> trait Hello {
     |     val mes: String = "Hello, World!"
     |     def hello(): Unit = println(mes)
     | }
defined trait Hello

ただし、そのまま使うことはできない。

scala> Hello.hello
<console>:12: error: not found: value Hello
       Hello.hello
       ^

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

トレイトの場合は継承というよりミックスインということが多いらしい。extends キーワードを使う継承は1つのクラスしかすることができないけど、with キーワードを使うミックスインは複数できる。

scala> trait TraitA
defined trait TraitA

scala> trait TraitB
defined trait TraitB

scala> class ClassA
defined class ClassA

scala> class ClassB
defined class ClassB

scala> class ClassC extends ClassA with TraitA with TraitB
defined class ClassC

ここで ClassC は ClassA を継承し、TraitA と TraitB をミックスインしている。一方、with キーワードを使っても ClassA と ClassB を継承することはできない。

scala> class ClassD extends CrassA with ClassB
<console>:12: error: not found: type CrassA
       class ClassD extends CrassA with ClassB
                            ^

直接インスタンス化できない

scala> val hello = new Hello
<console>:12: error: trait Hello is abstract; cannot be instantiated
       val hello = new Hello
                   ^

クラスパラメータ(コンストラクタ引数)をとることができない

直接インスタンス化できないんだからコンストラクタ引数をとれないのは当然だけど、じゃあ、定義時に決まらないフィールドはどうすればいいかというと、継承先のクラスで上書きしてやればいい。

scala> trait TraitA {
     |     val name: String
     |     def printName(): Unit = println(name)
     | }
defined trait TraitA

scala> class ClassA(val name: String) extends TraitA
defined class ClassA

scala> val a = new ClassA("Bill")
a: ClassA = ClassA@21bc3814

scala> a.printName
Bill

TraitA の name フィールドは実装のない抽象フィールドだけど、trait のまえに abstract キーワードをつけなくて構わない。フィールドの実装は ClassA で与えられている。ここで気が付いたけど、1つだけ継承するときはそれがトレイトでも extends キーワードを使う。

さてここまででトレイトの基本を見てきた。つぎはトレイトの機能を見ることにしよう。というところでいったんここまで。

コンパニオンオブジェクト

同じファイル内において、クラスと同じ名前で定義されたシングルトンオブジェクトをコンパニオンオブジェクトと呼ぶ。コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っていて、private なフィールドに対してもアクセスできる。

試してみよう。なお、REPL で試すには :paste コマンドを使って、ペーストモードになってからクラスとコンパニオンオブジェクトを一緒に定義する。こうしないと、REPL が正しく認識できないらしい。先のエントリーで警告が出てたのはこのせいだな。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Person(val name: String, var age: Int, private var weight: Int)

object Person {
    def printWeight(): Unit = {
        val andy = new Person("Andy", 27, 65)
        println(andy.weight)
    }
}

// Exiting paste mode, now interpreting.

defined class Person
defined object Person

ペーストモードを抜けるには Ctrl-D。

で、上のように定義したのがクラス Person とそのコンパニオンオブジェクト Person だ。コンパニオンオブジェクトの中から、プライベートは weight フィールドにアクセスしている。

じゃあ試してみよう。

scala> Person.printWeight
65

この通り、weight の値が出力された。

シングルトンオブジェクト

Scala では、クラスのほかに、object キーワードを使うことでシングルトンオブジェクトを作ることができる。シングルトンオブジェクトは、クラスとは違ってインスタンス化せずにそのまま使えるオブジェクトだ。

使い道としては次の2つがあげられる。

  • ユーティリティメソッドやグローバルな状態の置き場
  • 同名クラスのインスタンスのファクトリメソッド

まず、1つ目の使い道を見てみよう。シングルトンオブジェクトの定義構文はクラスの定義とほぼ同じで、class キーワードの代わりに object キーワードを使う。

scala> object Foo {
     |     def hello(): Unit = println("Hello, World!")
     | }
defined object Foo

この例では使っていないけど、クラスやトレイトを継承することもできる。そしてこの Foo オブジェクトはインスタンス化することなくそのまま使うことができる。

scala> Foo.hello
Hello, World!

もう1つの使い道、ファクトリメソッドについても見てみよう。同名のクラス Point をシングルトンオブジェクト Point を定義する。

scala> class Point(val x: Int, val y: Int) {
     |     override def toString(): String = "(" + x + ", " + y + ")"
     | }
defined class Point

scala> object Point {
     |     def apply(x: Int, y: Int): Point = new Point(x, y)
     | }
defined object Point
warning: previously defined class Point is not a companion to object Point.
Companions must be defined together; you may wish to use :paste mode for this.

警告が出ているけどとりあえずわきに置いておく。

シングルトンオブジェクトの apply というメソッドは Scala によって特別扱いされ、Point(x, y) という記述があったときに Point.apply(x, y) と解釈される。つまり、Point クラスのインスタンスを作るのに、new Point(2, 3) とする代わりにシングルトンオブジェクトを使って Point(2, 3) とすることができる。試してみよう。

scala> val p = Point(2, 3)
p: Point = (2, 3)

scala> p.toString
res1: String = (2, 3)

ちなみに toString というメソッドはどんなクラスにも定義されているらしい。上の Point クラスは、明示的には何も継承していないけど暗黙に何かのクラスを継承していて(たぶん)、そのクラスで toString メソッドが定義されているので、override キーワードを使って再定義している。

toString メソッドをオーバーライドすると、REPL のレスポンスに現れる値の表示も変わって (2, 3)  となる。これは REPL が値を表示するのに、暗黙に toString を呼び出しているからだね(たぶん)。

抽象メンバー、抽象クラス

クラスの定義時点では実装を持たず、サブクラスで実装されるようなメソッドやフィールドを抽象メンバーと呼ぶ。また、抽象メンバーを1つ以上持つクラスを抽象クラスと呼ぶ。

Scala では、抽象クラスは abstract キーワードをつけて定義する。そして、抽象メンバーの定義は、メソッドやフィールドの本体部分がない形とする。

以下に、抽象フィールド mes を持つ抽象クラス Hoge を見よう。

scala> abstract class Hoge {
     |     val mes: String
     |     def shout(): Unit = println(mes + "!!!!")
     | }
defined class Hoge

そしてこの mes に実装を与えるのは、Hoge を継承したサブクラス Fuga だ。

scala> class Fuga extends Hoge {
     |     val mes: String = "Fuga"
     | }
defined class Fuga

さて、じゃあ Fuga を使ってみよう。

scala> val fuga = new Fuga
fuga: Fuga = Fuga@409c6c89

scala> fuga.shout
Fuga!!!!

こんな感じだ。

そして最後に追加情報。クラス定義やインスタンスの生成時にも引数がなければカッコを省略できる。

クラスの継承

単一継承

Scala のクラスの継承は単一継承のようだ。もう少し言うと、スーパークラスのほかにトレイトというものを複数継承できるようだけど、トレイトについては日を改める。

クラスを継承するためには、extends キーワードに続けてスーパークラス名をつければいい。例を示そう。

scala> class Foo() {
     |     def foo(): Unit = println("Foo")
     | }
defined class Foo

scala> class Bar() extends Foo {
     |     def bar(): Unit = println("Bar")
     | }
defined class Bar

ここではクラス Bar がクラス Foo を継承している。Foo にはメソッド foo があり、Bar には Foo から継承したメソッド foo と Bar で定義されたメソッド bar がある。

scala> val foo = new Foo()
foo: Foo = Foo@3e81c10b

scala> foo.foo
Foo

scala> val bar = new Bar()
bar: Bar = Bar@5d842ce8

scala> bar.foo
Foo

scala> bar.bar
Bar

override

継承したクラス(サブクラス)で、スーパークラスのメソッドをオーバーライドしたいときには、明示的に override キーワードをつける必要がある。これによって、オーバライドするつもりで新しいメソッドを定義してしまったり、逆に新しいメソッドを定義するつもりでオーバーライドしてしまうようなことを防いでいるようだ。

scala> class Baz() extends Foo {
     |     override def foo(): Unit = println("Baz")
     | }
defined class Baz

scala> val baz = new Baz()
baz: Baz = Baz@7b6dd92d

scala> baz.foo
Baz

この例では、Baz でメソッド foo をオーバーライドしている。

ところで、何気なく書いたけど、引数のないメソッドの呼び出しはカッコを省略できるんだな。

複数の引数リストを持つメソッドと部分適用

Scala では複数の引数リストを持つメソッドを定義できる。読んで字の如く、引数リストが複数あるってことだ。

scala> class Adder {
     |     def add(x: Int)(y: Int) = x + y
     | }
defined class Adder

こんなふうに、引数リストを囲むカッコを連ねる。呼び出すときも同様。

scala> val adder = new Adder()
adder: Adder = Adder@2a83bab5

scala> adder.add(2)(3)
res0: Int = 5

引数リストの代わりに _ を使うと部分適用ができる。

scala> val fun = adder.add(3) _
fun: Int => Int = $$Lambda$3397/284877983@4dddc7e7

この fun は関数オブジェクトのようなものなのかな。残りと引数を与えると値が返ってくる。

scala> fun(4)
res1: Int = 7

いまは後ろの引数リストの代わりに _ を使ったけど、前の引数リストの代わりには使えないんだろうか。

scala> val fun2 = adder.add _ (3)
<console>:1: error: ';' expected but '(' found.
       val fun2 = adder.add _ (3)
                              ^

ダメか。

部分適用は、単一の引数リストでも使える。Adder クラスを定義しなおしてみよう。

scala> class Adder {
     |     def add(x: Int, y: Int) = x + y
     | }
defined class Adder

この新しい Adder クラスの add メソッドで部分適用を試してみる。

scala> val fun: Int => Int = adder.add(2, _)
fun: Int => Int = $$Lambda$3365/582762225@4e9e818e

scala> fun(3)
res0: Int = 5

関数オブジェクト(?)の型はを省略できないみたいだ。省略するとエラーになる。

scala> val fun = adder.add(2, _)
<console>:12: error: missing parameter type for expanded function ((x$1: ) => adder.add(2, x$1))
       val fun = adder.add(2, _)
                              ^

ところで、こっちの形式なら後ろの引数を部分適用できるんだろうか。

scala> val fun2: Int => Int = adder.add(_, 3)
fun2: Int => Int = $$Lambda$3381/567903408@64679683

scala> fun2(5)
res1: Int = 8

できた。

メソッド定義

クラスにはメソッドを定義できる。簡単な例からいこう。

scala> class Person(val name: String) {
| def hello(): String = {
| "Hello, I'm " + name + "!"
| }
| }
defined class Person

def で始まるのがメソッド定義だ。この例では引数をとらない hello メソッドを定義している。= を使うのがちょっと珍しい。

呼び出すには . (ドット)を使う。

scala> val andy = new Person("Andy")
andy: Person = Person@8d3da30

scala> andy.hello()
res0: String = Hello, I'm Andy!

メソッドの本体を { } で囲っているけど、これは一般には複数の式からなるから必要なのであって、メソッド定義の構文として必要なわけではない。言い換えると、メソッド本体が式一つだけなら { } はなくてもいい。こんなふうに。

scala> class Point(val x: Int, val y: Int) {
| def +(p: Point): Point = new Point(x + p.x, y + p.y)
| }
defined class Point

さらっと書いたけど、演算子がメソッドとして定義できるんだな。なんか条件とかあるんだろうか。

ま、とにかくこんなふうに使える。

scala> val p1 = new Point(1, 1)
p1: Point = Point@6b93c152

scala> val p2 = new Point(2, 2)
p2: Point = Point@cec7389

scala> val p3 = p1 + p2
p3: Point = Point@1514f0c2

scala> p3.x
res0: Int = 3

scala> p3.y
res1: Int = 3

今日は時間がないのでここまで。

クラス

クラス定義

Scala はオブジェクト指向プログラミング言語でもあるので、当然クラスの定義もできる。もっとも簡単な例は次のようになる。

scala> class Point(_x: Int, _y: Int) {
| val x = _x
| val y = _y
| }
defined class Point

これは x と y というフィールドを持つクラスだ。フィールドの宣言には val と var が使える。もちろん val で宣言したフィールドは代入できない。

クラスをインスタンス化するには new キーワードを使う。Point というクラス名の後につづく ( ) 内の引数がそのままコンストラクタの引数になる。

scala> val p = new Point(3, 2)
p: Point = Point@1409fe14

コンストラクタの引数名とフィールド名が同じでよければ、次のように簡単に書ける。

scala> class Point(val x: Int, val y: Int)
defined class Point

フィールドのアクセス制御

フィールドはデフォルトでパブリックなので、外側から参照することができる。

scala> p.x
res0: Int = 3

scala> p.y
res1: Int = 2

パブリックにしたくなければ、フィールドの宣言に private または protected キーワードをつける。private をつけるとそのクラスの中だけから、protected をつけるとクラスの派生クラスだけからアクセスできるようになる。private をつけた例を見てみよう。

scala> class Person(_name: String, _age: Int) {
| val name = _name
| private var age = _age
| }
<console>:13: warning: private var age in class Person is never used
private var age = _age
^

おっと、警告が出た。プライベートは変数 age が使われない、みたいなことを言われてるようだ。ま、そりゃそうだ。今はフィールドを操作するメソッドがないから、外からアクセスできないフィールドだけあっても役に立たないからな。でも、とりあえずクラス定義はできて、インスタンス化もできる。

scala> val andy = new Person("Andy", 27)
andy: Person = Person@4bd808d0

scala> andy.name
res0: String = Andy

scala> andy.age
<console>:13: error: variable age in class Person cannot be accessed in Person
andy.age
^

パブリックな name にはアクセスできて、プライベートな age にはアクセスできない様子がわかる。