Essential Scala
第3章 オブジェクトとクラス
2018.7.18
Takuya Tsuchida (@takuya0301)
第3章 オブジェクトとクラス
3.1 クラス
3.2 関数としてのオブジェクト
3.3 コンパニオンオブジェクト
3.4 ケースクラス
3.5 パターンマッチング
3.6 まとめ
2
第3章 オブジェクトとクラス
前章では、オブジェクトを作る方法と、メソッド呼び出しを通じて作用させる方法を見てき
ました。本章ではクラスを使用して、オブジェクトをより抽象化する方法を見ていきます。
クラスはオブジェクトを構築するためのテンプレートです。クラスがあれば、同じ型で共通
のプロパティを持つオブジェクトをたくさん作ることができます。
3
3.1 クラス
クラスは、類似したメソッドやフィールドを持つオブジェクト生成のためのテンプレートで
す。Scala におけるクラスも型を定義します。また、クラスから生成されたオブジェクトは
すべて同じ型を共有します。
4
3.1.1 クラス定義
これが Person クラスの定義です。
class Person {
val firstName = "Noel"
val lastName = "Welsh"
def name = firstName + " " + lastName
}
オブジェクト宣言のようにクラス宣言は名前を束縛します。しかし、クラス名は式で利用
できません。クラスは値ではなく、違う名前空間に存在しています。
Person
// error: not found: value Person
// Person
// ^
3.1.1 クラス定義
new 演算子を使用して新しい Person オブジェクトを作成できます。いつもの方法でメ
ソッドとフィールドにアクセスできます。
val noel = new Person
// noel: Person = Person@3235186a
noel.firstName
// res: String = Noel
3.1.1 クラス定義
オブジェクトの型は Person です。new 呼び出しごとに、同じ型で別個のオブジェクトが
生成されます。
noel
// res: Person = Person@3235186a
val newNoel = new Person
// newNoel: Person = Person@2792b987
val anotherNewNoel = new Person
// anotherNewNoel: Person = Person@63ee4826
3.1.1 クラス定義
これで Person を引数とするメソッドを書くことができます。
object alien {
def greet(p: Person) =
"Greetings, " + p.firstName + " " + p.lastName
}
alien.greet(noel)
// res: String = Greetings, Noel Welsh
alien.greet(newNoel)
// res: String = Greetings, Noel Welsh
ノート:Java メモ
Scala クラスは java.lang.Object のサブクラスであり、ほとんどのものは Java から
Scala のそれと同じように使用できる。Person におけるデフォルトの出力の振る舞いは
java.lang.Object で定義されている toString メソッドによるものである。
13
3.1.2 コンストラクター
コンストラクターは新しいオブジェクトを生成するときに引数を渡すことを可能にします。
class Person(first: String, last: String) {
val firstName = first
val lastName = last
def name = firstName + " " + lastName
}
val dave = new Person("Dave", "Gurnell")
// dave: Person = Person@3ed12df7
dave.name
// res: String = Dave Gurnell
3.1.2 コンストラクター
コンストラクター引数はクラス本体だけで使用できます。オブジェクトの外側からアクセス
するためには、val や def を使用して、フィールドやメソッドを定義する必要があります。
val キーワードをコンストラクター引数に付与することで、それらのフィールドを自動的に
定義できます。
class Person(val firstName: String, val lastName: String) {
def name = firstName + " " + lastName
}
new Person("Dave", "Gurnell").firstName
// res: String = Dave
3.1.2 コンストラクター
val フィールドは不変です。一度初期化されたら値を変更することができません。可変
フィールドを定義するために var キーワードも用意されています。
Scala プログラマーが、不変かつ副作用のないコードを好むことで、置換モデル*を使用
することができます。
* 参考:『計算機プログラムの構造と解釈』1.1.5 手続き作用の置換えモデル https://sicp.iijlab.net/fulltext/x115.html
ノート:クラス宣言文法
class Name(parameter: type, ...) {
declarationOrExpression ...
}
or
class Name(val parameter: type, ...) {
declarationOrExpression ...
}
● Name はクラスの名前
● parameter はコンストラクター引数の名前(省略可能)
● type はコンストラクター引数の型
● declarationOrExpression は宣言や定義(省略可能)
19
3.1.3 デフォルト引数とキーワード引数
すべてのメソッドとコンストラクターはキーワード引数とデフォルト引数値をサポートして
います。
メソッドやコンストラクターを呼び出すときに、キーワードとして引数名を使用することで
恣意的な順番で引数を指定できます。
new Person(lastName = "Last", firstName = "First")
// res: Person = Person(First,Last)*
* 訳注:この評価結果にはならない。後述するケースクラスで Person を定義するとこの評価結果にできる。
3.1.3 デフォルト引数とキーワード引数
デフォルト引数値を指定して定義できます。
def greet(firstName: String = "Some", lastName: String = "Body") =
"Greetings, " + firstName + " " + lastName + "!"
greet("Awesome")
// res: String = Greetings, Awesome Body!
キーワードとデフォルト引数値を組み合わせることで、前の引数をスキップして後の引数
を指定することができます。
greet(lastName = "Dave")
// res: String = Greetings, Some Dave!
ノート:キーワード引数
キーワード引数は引数の数や順番の変更に対して頑健にする。例えば、title 引数を
greet メソッドに追加した場合、キーワードのないメソッド呼び出しの意味は変わるが、
キーワードのあるメソッド呼び出しの意味は同じである。
def greet(title: String = "Citizen",
firstName: String = "Some", lastName: String = "Body") =
"Greetings, " + title + " " + firstName + " " + lastName + "!"
greet("Awesome") // これは現状正しくない
// res: String = Greetings, Awesome Some Body
greet(firstName = "Awesome") // これは依然正しい
// res: String = Greetings, Citizen Awesome Body
これは引数の多いメソッドやコンストラクターを作成するときに便利である。
24
3.1.4 Scala の型階層
Scala は Any という最上位の基底型を持ち、AnyVal と AnyRef という2つの型がその
下に存在します。AnyVal はすべての値型の基底型で、AnyRef はすべての参照型やク
ラスの基底型です。
いくつかの型は単純に Java に存在する型のエイリアスで、Int は int、Boolean は
boolean、AnyRef は java.lang.Object です。
3.1.4 Scala の型階層
Scala には2つの特殊な型が最下層に存在します。Nothing は throw 式の型です。Null
は null 値の型です。それらの特殊な型はすべての型の派生型で、下記のコードが描き
出すように健全さが維持されています。
def badness = throw new Exception("Error")
// badness: Nothing
null
// res: Null = null
val bar = if (true) 123 else badness
// bar: Int = 123
val baz = if (false) "it worked" else null
// baz: String = null
3.1.5 キーポイント:クラス
本節ではクラス定義を学び、同じ型のオブジェクトを生成できるようになりました。クラス
によって同じ属性を持つオブジェクトを横断的に抽象化できます。
オブジェクトの属性はフィールドとメソッドです。フィールドは計算済みの値を、メソッドは
呼び出すことができる計算をオブジェクト内に保持します。
new キーワードでコンストラクターを呼び出すことで、クラスからオブジェクトを生成しま
す。
また、キーワード引数やデフォルト引数についても学びました。
最後に、Java の型階層と共通する Scala の型階層について学びました。実際に、型階
層のサブツリーは Java と Scala のクラスが占有しています。
3.2 関数としてのオブジェクト
Scala は計算を所有するオブジェクトをつくるための言語機能を持ちます。それらのオブ
ジェクトは関数と呼ばれ、関数プログラミングの基礎になります。
3.2.1 apply メソッド
Scala の関数プログラミングをサポートする機能である関数適用文法を見ていきます。
Scala では apply メソッドを持つオブジェクトを関数のように呼び出せます。
class Adder(amount: Int) {
def apply(in: Int): Int = in + amount
}
val add3 = new Adder(3)
// add3: Adder = Adder@1d4f0fb4
add3(2) // add3.apply(2) のショートハンド
// res: Int = 5
ノート:関数適用文法
メソッド呼び出し object.apply(parameter, ...) は
object(parameter, ...) と書ける。
33
3.2.2 キーポイント:関数としてのオブジェクト
本節では、オブジェクトが関数であれば呼び出せるという関数適用文法を見ました。
関数適用文法は apply メソッドが定義されているどんなオブジェクトにも適用可能です。
関数適用文法によって、計算を第一級値として扱えます。メソッドと違い、オブジェクトは
データとして取り回せます。これによって Scala での本物の関数プログラミングに一歩
近づきました。
3.3 コンパニオンオブジェクト
特定のオブジェクトから独立して、クラスに論理的に所属するメソッドを定義したいときが
あります。Java では静的メソッドを使用しますが、Scala はシングルトンオブジェクトを使
用します。
3.3 コンパニオンオブジェクト
共通のユースケースは付加的なコンストラクターです。Scala でも複数のコンストラク
ターを定義できますが、Scala プログラマーはクラスと同じ名前のオブジェクトの apply
メソッドとして追加のコンストラクターを実装することを好みます。クラスのコンパニオンオ
ブジェクトとしてオブジェクトを参照します。
class Timestamp(val seconds: Long)
object Timestamp {
def apply(hours: Int, minutes: Int, seconds: Int): Timestamp =
new Timestamp(hours*60*60 + minutes*60 + seconds)
}
Timestamp(1, 1, 1).seconds
// res: Long = 3661
ノート:コンソールを効果的に使用する
コンパニオンオブジェクトは、そのオブジェクトが対応するクラスと同じコンパ
イル単位で定義しなければならない。普通のコードベースではクラスとオブジェ
クトを同じファイルに定義することを意味するが、REPL では :paste コマン
ドを使用してひとつのコマンドとして入力する必要がある。
REPL で :help と入力することでより多くを知ることができる。
37
3.3 コンパニオンオブジェクト
Scala は型名と値名という2つの名前空間を持ちます。この分離によってクラスとコンパ
ニオンオブジェクトを衝突なく名付けることができています。
コンパニオンオブジェクトはクラスのインスタンスではなく、独自の型を持つシングルトン
オブジェクトであるということは重要です。
Timestamp // 型は Timestamp ではなく Timestamp.type である
// res: Timestamp.type = Timestamp$@602b24e6
ノート:コンパニオンオブジェクト文法
クラスと同じ名前のオブジェクトを同じファイルに定義することで、クラスに対
応するコンパニオンオブジェクトを定義する。
class Name {
...
}
object Name {
...
}
41
3.3.1 キーポイント:コンパニオンオブジェクト
コンパニオンオブジェクトは、クラスのインスタンスに関連せずに、クラスに関連する機能
を提供します。それは追加のコンストラクターを提供するために使用されます。
コンパニオンオブジェクトは Java の静的メソッドを置換するものです。それは等価かつ
柔軟な機能を提供します。
コンパニオンオブジェクトは関連するクラスと同じ名前を持ちます。Scala は値と型につ
いて2つの名前空間を持つので名前の衝突は起きません。
コンパニオンオブジェクトは関連するクラスと同じファイルに定義する必要があります。
REPL で入力するときは :paste モードを使用し、クラスとコンパニオンオブジェクトを同
じブロックで入力してください。
3.4 ケースクラス
ケースクラスはクラス・コンパニオンオブジェクト・実用的なデフォルトを一括で定義する
ためにとても便利なショートハンドです。軽量のデータ保持クラスを作成するための理想
的な手段です。
ケースクラスはクラス定義に case キーワードを前置するだけで作成できます。
case class Person(firstName: String, lastName: String) {
def name = firstName + " " + lastName
}
3.4 ケースクラス
ケースクラスを定義するとき、Scala はクラスとコンパニオンオブジェクトを自動的に生成
します。
val dave = new Person("Dave", "Gurnell") // クラス
// dave: Person = Person(Dave,Gurnell)
Person // コンパニオンオブジェクト
// res: Person.type = Person
さらに、いくつかの便利な機能がクラスとコンパニオンオブジェクトに生成されます。
3.4.1 ケースクラスの機能
1. 各コンストラクター引数のフィールド
コンストラクター定義に val を書く必要はありませんが、そうしたとしても悪影響もありま
せん。
dave.firstName
// res: String = Dave
3.4.1 ケースクラスの機能
2. デフォルト toString メソッドはクラスのコンストラクターに似た実用的な表現を出力し
ます。
dave
// res: Person = Person("Dave","Gurnell")
3.4.1 ケースクラスの機能
3. オブジェクトのフィールド値を扱う実用的な equals メソッドと hashCode メソッド
List や Set、Map のようなコレクションでケースクラスを容易に取り扱えます。これは、オ
ブジェクトの参照ではなく、オブジェクトの内容にもとづいて比較できるということになりま
す。
new Person("Noel", "Welsh").equals(new Person("Noel", "Welsh"))
// res: Boolean = true
new Person("Noel", "Welsh") == new Person("Noel", "Welsh")
// res: Boolean = true
3.4.1 ケースクラスの機能
4. 同じフィールド値で新しいオブジェクトを生成する copy メソッド
dave.copy()
// res: Person = Person(Dave,Gurnell)
copy メソッドはクラスの新しいオブジェクトを作成して返すことに注意します。
dave.copy() eq res
// res: Boolean = false
3.4.1 ケースクラスの機能
copy メソッドはコンストラクター引数に対応する任意の引数を受け入れます。引数が指
定された場合は、新しいオブジェクトは既存の値の変わりにその値を使用します。いくつ
かのフィールド値を変更したオブジェクトのコピーを作成したいときはキーワード引数を
使用するのが理想的です。
dave.copy(firstName = "Dave2")
// res: Person = Person(Dave2,Gurnell)
dave.copy(lastName = "Gurnell2")
// res: Person = Person(Dave,Gurnell2)
ノート:値と参照の等価性
Scala の == 演算子は Java と異なる。それは参照の同一性を比較するかわりに
equals メソッドに委譲する。
Scala には Java の == と同じ振る舞いをする eq 演算子がある。しかし、めったにアプ
リケーションコードでは使用されない。
new Person("Noel", "Welsh") eq (new Person("Noel", "Welsh"))
// res: Boolean = false
dave eq dave
// res: Boolean = true
57
3.4.2 ケースクラスコンパニオンオブジェクトの機能
コンパニオンオブジェクトは、クラスのコンストラクターと同じ引数の apply メソッドを含み
ます。Scala プログラマーは、new を省略して簡潔にするために、コンストラクターより
apply メソッドを好むことで、式中でのコンストラクターを読みやすくします。
Person("Dave", "Gurnell") == Person("Noel", "Welsh")
// res: Boolean = false
Person("Dave", "Gurnell") == Person("Dave", "Gurnell")
// res: Boolean = true
コンパニオンオブジェクトはパターンマッチングで使用するための抽出子パターンの実装
コードも含みます。パターンマッチングについては本章で後述します。
ノート:ケースクラス宣言文法
case class Name(parameter: type, ...) {
declarationOrExpression …
}
● Name はケースクラスの名前
● parameter はコンストラクター引数の名前(省略可能)
● type はコンストラクター引数の型
● declarationOrExpression は宣言や定義(省略可能)
60
3.4.3 ケースオブジェクト
ケースオブジェクトはケースクラスのように定義され、ケースクラスと同じデフォルトメソッ
ドを持ちます。
case object Citizen {
def firstName = "John"
def lastName = "Doe"
def name = firstName + " " + lastName
}
3.4.3 ケースオブジェクト
ケースオブジェクトと通常のシングルトンオブジェクトには違いがあります。
case object キーワードはクラスとオブジェクトを定義し、そのオブジェクトはそのクラス
のインスタンスになります。
class Citizen { /* ... */ }
object Citizen extends Citizen { /* ... */ }
ケースオブジェクトはケースクラスに定義される機能のすべてを備えています。
* 訳注:ケースオブジェクトの用途としては列挙型の実装などがあります。
3.4.4 キーポイント:ケースクラス
ケースクラスは Scala のデータ型における必需品です。
ケースクラスを宣言するための文法は、クラスを宣言する文法と同じですが、case が前
置されます。
case class Name(parameter: type, ...) {
declarationOrExpression ...
}
ケースクラスはたくさんの自動生成されたメソッドと機能を持ちます。これらの振る舞い
を、関連メソッドを実装することで、ひとつひとつオーバーライドすることができます。
3.5 パターンマッチング
これまではメソッド呼び出しとフィールドアクセスでオブジェクトを作用させてきました。
ケースクラスはパターンマッチングという方法で作用させられます。
パターンマッチングはデータの「形」にもとづいて式を評価します。
case class Person(firstName: String, lastName: String)
object Stormtrooper {
def inspect(person: Person): String =
person match {
case Person("Luke", "Skywalker") => "Stop, rebel scum!"
case Person("Han", "Solo") => "Stop, rebel scum!"
case Person(first, last) => s"Move along, $first"
}
}
3.5 パターンマッチング
下記のように使用できます。
Stormtrooper.inspect(Person("Noel", "Welsh"))
// res: String = Move along, Noel
Stormtrooper.inspect(Person("Han", "Solo"))
// res: String = Stop, rebel scum!
ノート:パターンマッチング文法
expr0 match {
case pattern1 => expr1
case pattern2 => expr2
...
}
● expr0 はマッチする値に評価される
● パターン(ガード)pattern1, pattern2, ... はマッチする値に対して
順に検証される
● 最初にマッチしたパターンの右辺の式 expr1, expr2, ... が評価される*
パターンマッチングは式で、マッチした式の値に評価される。
68* 実際には、パターンは順番にテストするより効率的な形式にコンパイルされるが、その意味は同じである。
3.5.1 パターン文法
Scala はパターン(ガード)について表現力のある文法を持っています。ケースクラスの
コンストラクター文法とパターン文法はマッチします。
Person("Noel", "Welsh")
Person 型に対してマッチするパターンは下記のように書けます。
Person(pat0, pat1)
それぞれ pat0 と pat1 は firstName と lastName にマッチするパターンです。
3.5.1 パターン文法
pat0 や pat1 の場所では4つのパターンが使用できます。
1. 名前。名前のある位置のどんな値にもマッチし、その名前に値が束縛される。
2. アンダースコア。アンダースコアのある位置のどんな値にもマッチし、その値は無視
される。
3. リテラル。リテラルの値とマッチする。
4. ケースクラスのコンストラクタースタイル文法。
3.5.2 キーポイント:パターンマッチング
ケースクラスはパターンマッチングという相互作用の新しい形式を可能にします。パターン
マッチングは一致するケースクラスに応じて異なる式を評価します。
expr0 match {
case pattern1 => expr1
case pattern2 => expr2
...
}
パターンは下記のいずれかになります。
1. 名前。どんな値もマッチし、名前に束縛される。
2. アンダースコア。どんな値もマッチし、無視される。
3. リテラル。リテラルの値とマッチする。
4. ケースクラスのコンストラクタースタイル文法。
3.6 まとめ
本章ではクラスを探検しました。クラスによるオブジェクトの抽象化を見てきました。これ
によって、共通の属性を共有し、共通の型を持つオブジェクトを生成できます。
また、クラスに所属しない汎用メソッドや、追加のコンストラクターを定義するために使用
されるコンパニオンオブジェクトも見ました。
最後に、ボイラープレートコードを削減し、パターンマッチングを可能にするケースクラス
を紹介しました。

Essential Scala 第3章 オブジェクトとクラス