LoginSignup
3
2

More than 3 years have passed since last update.

オブジェクト指向から関数型プログラミングへ『Scala 入門 (Essential Scala)』第3章 オブジェクトとクラス

Last updated at Posted at 2020-06-08

CC BY-SA 4.0 で公開されている Essential Scala3 Objects and Classes を日本語訳したものです。日本語訳においてもライセンスは同じものを継承しています。毎日少しずつ翻訳を進めており、最新の日本語訳は Scala 入門 (Essential Scala) にあります。

原書:underscoreio/essential-scala
訳書:takuya0301/essential-scala-ja

ライセンス

Written by Dave Gurnell and Noel Welsh. Copyright Underscore Consulting LLP, 2015-2017.
Translated by Takuya Tsuchida. Copyright Takuya Tsuchida, 2020.
CC BY-SA 4.0 logo
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

目次

序文
第1章 はじめに
第2章 式・型・値
第3章 オブジェクトとクラス
第4章 トレイトによるデータモデリング
第5章 シーケンス処理
第6章 コレクション
第7章 型クラス

オブジェクトとクラス

前章では、オブジェクトを作成し、メソッド呼び出しを通じてオブジェクトを作用させる方法を見ました。本章では、クラス (class) を使用してオブジェクトを抽象化する方法を見ていきます。クラスはオブジェクトを構築するためのテンプレートです。クラスが与えられれば、同じ型を持ち、共通のプロパティを持つ多くのオブジェクトをつくることができます。

クラス

クラスは、似たようなメソッドやフィールドを持つオブジェクトを生成するためのテンプレートです。Scala では、クラスもまた型を定義し、クラスから生成されたすべてのオブジェクトは同じ型を共有します。これによって、前章の演習「やぁ、人間」が持つ問題を克服することができます。

クラスを定義する

これは、シンプルな Person クラスの宣言です。

class Person {
  val firstName = "Noel"
  val lastName = "Welsh"
  def name = firstName + " " + lastName
}

オブジェクト宣言のように、クラス宣言は名前(この場合 Person)を束縛し、また式ではありません。しかしながら、オブジェクト名と違って、式の中でクラス名を使用することはできません。クラスは値ではなく、クラスは異なる名前空間 (namespace) に住んでいます。

Person
// <console>:13: error: not found: value Person
//        Person
//        ^

new 演算子を使用して、新しい Person オブジェクトを生成できます。オブジェクトは値なので、通常の方法でそれらのメソッドやフィールドにアクセスできます。

val noel = new Person
// noel: Person = Person@15bfe8fd

noel.firstName
// res1: String = Noel

オブジェクトの型が Person であることに注意してください。印字された値には、@xxxxxxxx という形式のコードが含まれており、そのオブジェクトを特定する一意の識別子です。new を呼び出すたびに、同じ型の別オブジェクトが生成されます。

noel
// res2: Person = Person@15bfe8fd

val newNoel = new Person
// newNoel: Person = Person@60cea738

val anotherNewNoel = new Person
// anotherNewNoel: Person = Person@22fe5689

これは、引数として任意の Person を受け取るメソッドを書けることを意味します。

object alien {
  def greet(p: Person) =
    "Greetings, " + p.firstName + " " + p.lastName
}
alien.greet(noel)
// res3: String = Greetings, Noel Welsh

alien.greet(newNoel)
// res4: String = Greetings, Noel Welsh

:information_source: Java ヒント

Scala クラスはすべて java.lang.Object の派生クラスであり、ほとんどの場合、Scala と同様に Java からも使用できます。Person におけるデフォルト印字の振る舞いは、java.lang.Object に定義されている toString メソッドに由来しています。


コンストラクター

現状、Person クラスは全然役に立ちません。新しいオブジェクトを好きなだけ生成することができますが、すべて同じ firstNamelastName を持つためです。それでは、それぞれの人に異なる名前を与えるにはどうすればいいのでしょうか?

その解決策は、コンストラクター (constructor) を導入することで、それは新しいオブジェクトを生成するときに引数を渡すことを可能にします。

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@258c10ba

dave.name
// res5: String = Dave Gurnell

コンストラクター引数 firstlast はクラスの本体でのみ使用できます。オブジェクトの外側からデータにアクセスするには、valdef を使用してフィールドやメソッドを宣言しなければなりません。

コンストラクター引数とフィールドはしばしば冗長です。幸運なことに、Scala は両方を一度に宣言する便利な略記法を提供してくれています。コンストラクター引数の前に val キーワードをつけることで、Scala はそれらのフィールドを自動的に定義してくれるようになります。

class Person(val firstName: String, val lastName: String) {
  def name = firstName + " " + lastName
}
new Person("Dave", "Gurnell").firstName
// res6: String = Dave

val フィールドは不変 (immutable) で、一度初期化された後にそれらの値を変更することはできません。Scala は可変 (mutable) フィールドを定義するために var キーワードも提供しています。

Scala プログラマーは、不変かつ副作用のないコードを書くことを好む傾向があるので、置換モデルを使用してそれらを導出することができます。本書では、もっぱら不変な val フィールドに焦点を当てていきます。


:information_source: クラス宣言文法

クラスを宣言するための文法は、

class Name(parameter: type, ...) {
  declarationOrExpression ...
}

class Name(val parameter: type, ...) {
  declarationOrExpression ...
}

です。ここで、

  • Name はクラスの名前
  • parameter はコンストラクター引数に与えられた名前(オプション)
  • type はコンストラクター引数の型
  • declarationOrExpression は宣言や式(オプション)

とします。


デフォルト引数とキーワード引数

Scala のすべてのメソッドとコンストラクターは、キーワード引数 (keyword parameter)デフォルト引数値 (default parameter value) に対応しています。

メソッドやコンストラクターを呼び出すときに、任意の順番で引数を指定するためのキーワードとして引数名を使用することができます。

new Person(lastName = "Last", firstName = "First")
// res7: Person = Person@5cce4fcc

これは、下記のように定義されたデフォルト引数値と組み合わせて使用するといっそう便利です。

def greet(firstName: String = "Some", lastName: String = "Body") =
  "Greetings, " + firstName + " " + lastName + "!"

引数がデフォルト値を持つのであれば、メソッド呼び出しでその引数を省略できます。

greet("Busy")
// res8: String = Greetings, Busy Body!

デフォルト引数値とキーワードを組み合わせることで、前の引数を省略し、後の引数だけに値を渡すこともできます。

greet(lastName = "Dave")
// res9: String = Greetings, Some Dave!

:information_source: キーワード引数

キーワード引数は、引数の数や順番の変更に対して堅牢です。例えば、greet メソッドに title 引数を追加すると、キーワードなしのメソッド呼び出しにおいて意味が変化してしまいますが、キーワードありの呼び出しでは同じままです。

def greet(title: String = "Citizen", firstName: String = "Some", lastName: String = "Body") =
  "Greetings, " + title + " " + firstName + " " + lastName + "!"

greet("Busy") // これは正しくなくなりました
// res10: String = Greetings, Busy Some Body!

greet(firstName = "Busy") // これは正しいままです
// res11: String = Greetings, Citizen Busy Body!

これは、多数の引数を伴うメソッドやコンストラクターを作成するとき、特に便利です。


Scala の型階層

プリミティブ型とオブジェクト型を区別する Java とは異なり、Scala ではすべてがオブジェクトです。その結果、IntBoolean のような「プリミティブ」値の型は、クラスやトレイトと同じ型階層の一部を構成しています。

Scala 型階層

Scala は Any と呼ばれる最上位の基底型を持ち、その下に AnyValAnyRef という2つの型があります。AnyVal はすべての値型の基底型で、AnyRef はすべての参照型やクラスの基底型です。Scala や Java におけるすべてのクラスは AnyRef の派生型です1

それらの型のいくつかは、単純に Java に存在する型の Scala における別名で、IntintBooleanbooleanAnyRefjava.lang.Object です。

階層の最下位 (bottom) に2つの特別な型があります。Nothingthrow 式の型で、Nullnull 値の型です。それらの特別な型は、他のすべての型の派生型で、コードにおける他の型を健全に保ちながらも、thrownull に型を割り当てることを補助します。下記のコードはこのことを説明しています。

def badness = throw new Exception("Error")
// badness: Nothing

def otherbadness = null
// otherbadness: Null

val bar = if(true) 123 else badness
// bar: Int = 123

val baz = if(false) "it worked" else otherbadness
// baz: String = null

badnessotherbadness の型はそれぞれ NothingNull であるにも関わらず、barbaz の型は実用的なままです。なぜならば、IntIntNothing の最小共通基底型で、StringStringNull の最小共通基底型であるからです。

覚えておいてほしいこと

本節では、同じを持つ様々なオブジェクトの生成を可能にするクラスを定義する方法を学びました。このように、クラスを使うことで、似たような性質を持つオブジェクトを横断的に抽象化することができます。

クラスにおいてもオブジェクトの性質は、フィールドメソッドの形をとります。フィールドはオブジェクトに格納される事前に計算された値で、メソッドは呼び出すことができる計算です。

クラスを宣言するための文法は、

class Name(parameter: type, ...) {
  declarationOrExpression ...
}

です。

new キーワードを使用し、コンストラクターを呼び出すことによって、クラスからオブジェクトを生成します。

また、キーワード引数デフォルト引数についても学びました。

最後に、Scala の型階層について、Java の型階層との重複や、特殊な型である AnyAnyRefAnyValNothingNullUnit を含むこと、そして Java のクラスと Scala のクラスは、どちらも型階層の同じサブツリーを占有するという事実を学びました。

演習

今ではクラスで楽しく遊ぶために十分な機構を手に入れました。

猫、再び

以前の演習に登場した猫を思い出しましょう。

名前 (Name) 色 (Colour) 食べ物 (Food)
オズワルド (Oswald) 黒 (Black) ミルク (Milk)
ヘンダーソン (Henderson) 茶トラ (Ginger) カリカリ (Chips)
クエンティン (Quentin) トラ (Tabby and white) カレー (Curry)

Cat クラスを定義し、上の表の各猫についてオブジェクトを生成してください。

解答(クリックして表示)

これはクラスを定義する文法に慣れるための指の運動です。

class Cat(val colour: String, val food: String)

val oswald = new Cat("Black", "Milk")
val henderson = new Cat("Ginger", "Chips")
val quentin = new Cat("Tabby and white", "Curry")

猫、ぶらぶらする

willServe メソッドを伴う ChipShop オブジェクトを定義してください。このメソッドは Cat を受け取り、猫の好きなエサがカリカリ (chips) であれば true を返し、そうでなければ false を返します。

解答(クリックして表示)

object ChipShop {
  def willServe(cat: Cat): Boolean =
    if(cat.food == "Chips")
      true
    else
      false
}

監督デビュー

下記のフィールドとメソッドを伴う、DirectorFilm の2つのクラスを書いてください。

  • Director に含まれる:

    • String 型の firstName フィールド
    • String 型の lastName フィールド
    • Int 型の yearOfBirth フィールド
    • 無引数で、フルネームを返す name メソッド
  • Film に含まれる:

    • String 型の name フィールド
    • Int 型の yearOfRelease フィールド
    • Double 型の imdbRating フィールド
    • Director 型の director フィールド
    • 公開時における監督の年齢を返す directorsAge メソッド
    • 引数として Director を受け取り、Boolean を返す isDirectedBy メソッド

下記のデモデータをコードにコピー&ペーストし、そのコードを変更せずに動作するようコンストラクターを調整してください。

val eastwood          = new Director("Clint", "Eastwood", 1930)
val mcTiernan         = new Director("John", "McTiernan", 1951)
val nolan             = new Director("Christopher", "Nolan", 1970)
val someBody          = new Director("Just", "Some Body", 1990)

val memento           = new Film("Memento", 2000, 8.5, nolan)
val darkKnight        = new Film("Dark Knight", 2008, 9.0, nolan)
val inception         = new Film("Inception", 2010, 8.8, nolan)

val highPlainsDrifter = new Film("High Plains Drifter", 1973, 7.7, eastwood)
val outlawJoseyWales  = new Film("The Outlaw Josey Wales", 1976, 7.9, eastwood)
val unforgiven        = new Film("Unforgiven", 1992, 8.3, eastwood)
val granTorino        = new Film("Gran Torino", 2008, 8.2, eastwood)
val invictus          = new Film("Invictus", 2009, 7.4, eastwood)

val predator          = new Film("Predator", 1987, 7.9, mcTiernan)
val dieHard           = new Film("Die Hard", 1988, 8.3, mcTiernan)
val huntForRedOctober = new Film("The Hunt for Red October", 1990, 7.6, mcTiernan)
val thomasCrownAffair = new Film("The Thomas Crown Affair", 1999, 6.8, mcTiernan)
eastwood.yearOfBirth
// res16: Int = 1930

dieHard.director.name
// res17: String = John McTiernan

invictus.isDirectedBy(nolan)
// res18: Boolean = false

さらに、copy と呼ばれる Film のメソッドを実装します。このメソッドは、コンストラクターと同じ引数を受け取り、映画の新しいコピーを生成します。各引数にデフォルト値を与えて、映画の値の一部を変更してコピーできるようにします。

highPlainsDrifter.copy(name = "L'homme des hautes plaines")
// res19: Film = Film(L'homme des hautes plaines,1973,7.7,Director(Clint,Eastwood,1930))

thomasCrownAffair.copy(yearOfRelease = 1968,
  director = new Director("Norman", "Jewison", 1926))
// res20: Film = Film(The Thomas Crown Affair,1968,6.8,Director(Norman,Jewison,1926))

inception.copy().copy().copy()
// res21: Film = Film(Inception,2010,8.8,Director(Christopher,Nolan,1970))

解答(クリックして表示)

この演習は、Scala のクラスやフィールド、メソッドの記述をいくつかのハンズオンによる体験として提供します。模範解答は下記のようになります。

class Director(
  val firstName: String,
  val lastName: String,
  val yearOfBirth: Int) {

  def name: String =
    s"$firstName $lastName"

  def copy(
    firstName: String = this.firstName,
    lastName: String = this.lastName,
    yearOfBirth: Int = this.yearOfBirth): Director =
    new Director(firstName, lastName, yearOfBirth)
}

class Film(
  val name: String,
  val yearOfRelease: Int,
  val imdbRating: Double,
  val director: Director) {

  def directorsAge =
    yearOfRelease - director.yearOfBirth

  def isDirectedBy(director: Director) =
    this.director == director

  def copy(
    name: String = this.name,
    yearOfRelease: Int = this.yearOfRelease,
    imdbRating: Double = this.imdbRating,
    director: Director = this.director): Film =
    new Film(name, yearOfRelease, imdbRating, director)
}

シンプル・カウンター

Counter クラスを実装してください。コンストラクターは Int を受け取るようにしてください。カウンターを増加させる inc メソッドとカウンターを減少させる dec メソッドを、それぞれ新しい Counter を返すように実装してください。こちらが利用例です。

new Counter(10).inc.dec.inc.inc.count
// res23: Int = 12

解答(クリックして表示)
class Counter(val count: Int) {
  def dec = new Counter(count - 1)
  def inc = new Counter(count + 1)
}

クラスやオブジェクトによる実践の傍ら、この演習には2つ目の目的があります。それは、なぜ incdec が、同じカウンターを直接更新する代わりに、新しい Counter を返すのかを考えることです。

val フィールドは不変なので、count の新しい値を伝える他の方法を思い付く必要があります。新しい Counter オブジェクトを返すメソッドは、代入による副作用なしに新しい状態を返す方法を与えてくれます。それはまたメソッドチェーン (method chaining) を可能にし、一連の更新全体をひとつの式で書くことを可能にします。

実際のところ、利用例 new Counter(10).inc.dec.inc.inc.count は、最後の Int 値を返すまでに Counter のインスタンスを5つ生成します。このような単純計算のための、余分なメモリと CPU のオーバーヘッドを気にするかもしれませんが、その心配はありません。JVM のような最新の実行環境では、このスタイルのプログラミングにおける余分なオーバーヘッドは、性能が最重要なコードを除き、無視して構わないものになっています。

高速カウント

前の演習に登場した Counter を拡張し、ユーザーが incdecInt 引数を任意に渡せるようにしてください。引数が省略されたときは、1 をデフォルトにしてください。

解答(クリックして表示)

最も単純な解答はこのようになります。

class Counter(val count: Int) {
  def dec(amount: Int = 1) = new Counter(count - amount)
  def inc(amount: Int = 1) = new Counter(count + amount)
}

しかし、これは incdec に丸括弧を追加します。引数を省略したときにも、空の丸括弧を付与しなければなりません。

new Counter(10).inc
// <console>:14: error: missing argument list for method inc in class Counter
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `inc _` or `inc(_)` instead of `inc`.
//        new Counter(10).inc
//                        ^

本来の丸括弧なしのメソッドを再現するには、メソッドのオーバーロード (method overloading) を使用して、丸括弧の付与を回避します。

class Counter(val count: Int) {
  def dec: Counter = dec()
  def inc: Counter = inc()
  def dec(amount: Int = 1): Counter = new Counter(count - amount)
  def inc(amount: Int = 1): Counter = new Counter(count + amount)
}

new Counter(10).inc.inc(10).count
// res25: Int = 21

追加カウント

こちらは Adder と呼ばれるシンプルなクラスです。

class Adder(amount: Int) {
  def add(in: Int) = in + amount
}

Counter を拡張し、adjust と呼ばれるメソッドを追加してください。このメソッドは Adder を受け入れ、countAdder を適用した結果を伴う新しい Counter を返します。

解答(クリックして表示)
class Counter(val count: Int) {
  def dec = new Counter(count - 1)
  def inc = new Counter(count + 1)
  def adjust(adder: Adder) =
    new Counter(adder.add(count))
}

これは興味深いパターンで、Scala の機能を学ぶにつれて、より強力になっていくでしょう。計算を表現するために Adder を使用し、それを Counter に受け渡しています。メソッドは式ではないという先の議論を思い出してください。それらはフィールドに格納したり、データとして受け渡したりできません。しかしながら、Adder はオブジェクトであると同時に計算でもあるのです。

オブジェクト指向プログラミング言語において、計算としてオブジェクトを使用することは一般的なパラダイムです。例えば、Java の Swing における古典的な ActionListener を考えてみてください。

public class MyActionListener implements ActionListener {
  public void actionPerformed(ActionEvent evt) {
    // 何か計算を実行する
  }
}

AddersActionListener のようなオブジェクトの欠点は、特定の状況での使用に限定されることです。Scala には、オブジェクトとして様々な計算の表現を可能にする、関数 (function) と呼ばれるより一般的な概念が含まれています。本章では、関数の背後にある概念のいくつかを紹介していきます。

関数としてのオブジェクト

前節、最後の演習で Adder と呼ばれるクラスを定義しました。

class Adder(amount: Int) {
  def add(in: Int): Int = in + amount
}

議論の中で、計算を表現するオブジェクトとしての Adder を説明しました。それは、値として渡すことのできるメソッドを得られたようなものでした。

計算のように振る舞うオブジェクトは強力な概念で、Scala にはそれを生成するための言語機能が完全に備わっています。それらのオブジェクトは関数 (function) と呼ばれ、関数型プログラミング (functional programming) の基礎を成します。

apply メソッド

ここでは、関数型プログラミングをサポートする Scala の一機能関数適用文法 (function application syntax) を見ていきましょう。

Scala では、慣例によって、apply と呼ばれるメソッドを持つオブジェクトを関数のように「呼び出す」ことができます。apply と名付けられたメソッドによって、呼び出し文法 foo.apply(args)foo(args) になるという特別な略記法が与えられます。

例えば、Adderadd メソッドを apply に改名してみましょう。

class Adder(amount: Int) {
  def apply(in: Int): Int = in + amount
}
val add3 = new Adder(3)
// add3: Adder = Adder@3390766f

add3.apply(2)
// res0: Int = 5

add3(4) // add3.apply(4) の略記法
// res1: Int = 7

この簡単なトリックによって、オブジェクトは文法的に関数らしく「見える」ようになります。オブジェクトを、変数に代入したり、引数として受け渡したり、メソッドではできなかったことがたくさんできます。


:information_source: 関数適用文法

メソッド呼び出し object.apply(parameter, ...)object(parameter, ...) と書くこともできる。


覚えておいてほしいこと

本節では、オブジェクトを関数であるかのように「呼び出す」ための関数適用文法を見ました。

関数適用文法は、apply メソッドが定義されたどんなオブジェクトでも利用可能です。

関数適用文法によって、計算のように振る舞う第一級値(訳注:第一級関数)を持てるようになりました。メソッドと違って、オブジェクトはデータとして受け渡すことができます。これで、Scala における真の関数型プログラミングに一歩近付きました。

演習

関数が関数でないのはどんなとき?

次節の最後に、いくつかのコードを書く機会があります。ここで、重要な理論上の疑問について考えてみましょう。

関数適用文法は、計算を実行する真に再利用可能なオブジェクトを生成できることに、どのくらい近付いているのでしょうか?何が足りないのでしょうか?

解答(クリックして表示)

主に足りていないものは、値を横断的に抽象化する方法であるです。

現時点では、数値を加算するという知識を捕捉するために Adder と呼ばれるクラスを定義できますが、他の開発者はその知識を使用するために、この特定のクラスについて知っている必要があるため、そのコードはコードベースを横断して可搬であるとは言えません。

HandlerCallbackAdderBinaryAdder などのような名前で共通する関数型のライブラリを定義できますが、これはすぐに非現実的になってしまいます。

後ほど、様々な状況で使用できる一般的な関数型一式を定義することで、Scala がこの問題にどのように対処しているのかを見ていきましょう。

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

論理的にはクラスに所属していても、どの特定のオブジェクトからも独立しているメソッドを作成したいことがあります。そのために Java では静的メソッド (static method) を使用しますが、Scala にはシングルトンオブジェクトというもっと単純な解決策があります。

一般的な使用例は補助コンストラクターです。Scala がクラスについて複数のコンストラクターを定義できる文法を持つにも関わらず、Scala プログラマーはほとんどの場合、クラスと同じ名前を持つオブジェクトに、追加のコンストラクターとして apply メソッドを実装することを好みます。そのオブジェクトをクラスのコンパニオンオブジェクト (companion object) と呼びます。例えば、下記のようになります。

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
// res1: Long = 3661

:information_source: コンソールを有効活用する

上記の例は、:paste コマンドを使用することに注意してください。コンパニオンオブジェクトは、後援するクラスと同じコンパイル単位で定義されなければなりません。通常のコードベースで、これはクラスとオブジェクトを同じファイルの中に定義することを意味しますが、REPL 上では :paste を使用し、それらをひとつのコマンドとして入力しなければなりません。

REPL 上に :help と入力することでより詳細を知ることができます。


前述のように、Scala は、型名 (type name) 空間と値名 (value name) 空間の2つの名前空間を持ちます。このように分離することで、衝突なくクラスとコンパニオンオブジェクトに同じ名前を付けることができます。

シングルトンオブジェクトは独自の型を伴うので、コンパニオンオブジェクトはクラスのインスタンスではないという重要なことに注意してください。

Timestamp // 型は `Timestamp.type` であり `Timestamp` ではないことに注意
// res2: Timestamp.type = Timestamp$@74d89b8f

:information_source: コンパニオンオブジェクト文法

クラスのためにコンパニオンオブジェクトを定義するには、同じ名前のオブジェクトをクラスと同じファイルに定義します。

class Name {
  ...
}

object Name {
  ...
}

覚えておいてほしいこと

コンパニオンオブジェクトは、機能をクラスのインスタンスに関連付けることなく、クラスに関連付ける手段を提供します。一般的に、それらは追加のコンストラクターを提供するために使用されます。

コンパニオンオブジェクトは Java の静的メソッドを代替します。それらは等価な機能を提供し、より柔軟です。

コンパニオンオブジェクトは、関連付けられたクラスと同じ名前を持ちます。 これは、値の名前空間と型の名前空間という2つの名前空間を Scala が持つため、名前の衝突を引き起こしません。

コンパニオンオブジェクトは、関連付けられたクラスと同じファイルに定義されなければいけません。 REPL 上で入力するときは :paste モードを使用して、クラスとコンパニオンオブジェクトが同じコードブロックで入力されなければいけません。

演習

フレンドリーな人ファクトリ

姓と名を個別にではなく、姓名全体をひとつの文字列として受け取る apply メソッドを含む、Person のコンパニオンオブジェクトを実装してください。

ヒント:下記のようにして StringArray の要素に分割できます。

val parts = "John Doe".split(" ")
// parts: Array[String] = Array(John, Doe)

parts(0)
// res3: String = John

解答(クリックして表示)

こちらがコードです。

class Person(val firstName: String, val lastName: String) {
  def name: String =
    s"$firstName $lastName"
}

object Person {
  def apply(name: String): Person = {
    val parts = name.split(" ")
    new Person(parts(0), parts(1))
  }
}

こちらが使用例です。

Person.apply("John Doe").firstName // 完全なメソッド呼び出し
// res5: String = John

Person("John Doe").firstName // 糖衣適用文法
// res6: String = John

業績の派生的な内容

下記のように、DirectorFilm のためのコンパニオンオブジェクトを記述してください。

  • Director コンパニオンオブジェクトに含まれる:

    • クラスのコンストラクターと同じ引数を受け取り、新しい Director を返す apply メソッド
    • 2つの Director を受け取り、2つのうち年齢が上の方を返す older メソッド
  • Film コンパニオンオブジェクトに含まれる:

    • クラスのコンストラクターと同じ引数を受け取り、新しい Film を返す apply メソッド
    • 2つの Film を受け取り、2つのうち imdbRating が高い方を返す highestRating メソッド
    • 2つの Film を受け取り、それぞれの撮影時に年齢が上の Director を返すoldestDirectorAtTheTime メソッド

解答(クリックして表示)

この演習は、たくさんのコードを書く練習を提供することを意図しています。前節のクラス定義を含む模範回答はこのようになります。

class Director(
  val firstName: String,
  val lastName: String,
  val yearOfBirth: Int) {

  def name: String =
    s"$firstName $lastName"

  def copy(
    firstName: String = this.firstName,
    lastName: String = this.lastName,
    yearOfBirth: Int = this.yearOfBirth) =
    new Director(firstName, lastName, yearOfBirth)
}

object Director {
  def apply(firstName: String, lastName: String, yearOfBirth: Int): Director =
    new Director(firstName, lastName, yearOfBirth)

  def older(director1: Director, director2: Director): Director =
    if (director1.yearOfBirth < director2.yearOfBirth) director1 else director2
}

class Film(
  val name: String,
  val yearOfRelease: Int,
  val imdbRating: Double,
  val director: Director) {

  def directorsAge =
    director.yearOfBirth - yearOfRelease

  def isDirectedBy(director: Director) =
    this.director == director

  def copy(
    name: String = this.name,
    yearOfRelease: Int = this.yearOfRelease,
    imdbRating: Double = this.imdbRating,
    director: Director = this.director) =
    new Film(name, yearOfRelease, imdbRating, director)
}

object Film {
  def apply(
    name: String,
    yearOfRelease: Int,
    imdbRating: Double,
    director: Director): Film =
    new Film(name, yearOfRelease, imdbRating, director)

  def newer(film1: Film, film2: Film): Film =
    if (film1.yearOfRelease < film2.yearOfRelease) film1 else film2

  def highestRating(film1: Film, film2: Film): Double = {
    val rating1 = film1.imdbRating
    val rating2 = film2.imdbRating
    if (rating1 > rating2) rating1 else rating2
  }

  def oldestDirectorAtTheTime(film1: Film, film2: Film): Director =
    if (film1.directorsAge > film2.directorsAge) film1.director else film2.director
}

型か値か?

クラスとコンパニオンオブジェクトの名前付けによる類似は、新しい Scala 開発者の混乱を引き起こしがちです。コードのブロックを読んでいるとき、その部分がクラス()を示しているのか、シングルトンオブジェクト()を示しているのかについて知っていることが重要です。

「型か値か?」 という新しくヒットしそうなクイズを思いついたので、これから試してみましょう。単語 Film の参照しているものが型か値かをそれぞれの事例で特定してください。

val prestige: Film = bestFilmByChristopherNolan()

解答(クリックして表示)

型!このコードは、型 Film の値 prestige を定義しています。

new Film("Last Action Hero", 1993, mcTiernan)

解答(クリックして表示)

型!これは、Filmコンストラクターへの参照です。このコンストラクターは、であるクラス Film の一部です。

Film("Last Action Hero", 1993, mcTiernan)

解答(クリックして表示)

値!これは、下記の略記法です。
Film.apply("Last Action Hero", 1993, mcTiernan)

apply は、シングルトンオブジェクト(値)Film に定義されたメソッドです。

Film.newer(highPlainsDrifter, thomasCrownAffair)

解答(クリックして表示)

値!newer は、シングルトンオブジェクト Film に定義された別のメソッドです。

最後は難しいものを……

Film.type

解答(クリックして表示)

値!これは巧妙です!これを間違えたとしても許されるでしょう。

Film.type は、シングルトンオブジェクト Film の型を参照するので、この場合の Film は値への参照です。しかしながら、コードの断片全体は型になります。

ケースクラス

ケースクラス (case class) は、クラスやコンパニオンオブジェクト、たくさんの実用的なデフォルト機能をまとめて定義してくれる非常に便利な略記法です。それは、やっかいごとを最小限にして、軽量なデータ保持クラスを生成するための理想的な方法になります。

ケースクラスは、クラス定義にキーワード case を前置するだけで生成されます。

case class Person(firstName: String, lastName: String) {
  def name = firstName + " " + lastName
}

ケースクラスを定義するとき、いつでも Scala は自動的にクラスとコンパニオンオブジェクトを生成します。

val dave = new Person("Dave", "Gurnell") // クラスを持つ
// dave: Person = Person(Dave,Gurnell)

Person // コンパニオンオブジェクトも
// res0: Person.type = Person

さらに、クラスとコンパニオンオブジェクトには、とても便利ないくつかの機能があらかじめ用意されています。

ケースクラスの機能

  1. 各コンストラクター引数のフィールド。コンストラクター定義に val と書く必要はありませんし、明示的に書いても悪影響はありません。

    dave.firstName
    // res1: String = Dave
    
  2. デフォルト toString メソッド。それは、クラスのコンストラクターに似た実用的な表現を印字します。もう @ 記号と謎めいた16進数とはおさらばです。

    dave
    // res2: Person = Person(Dave,Gurnell)
    
  3. 実用的な equals メソッドと hashCode メソッド。それらは、オブジェクトのフィールド値に基づいて動作します。

    これは、ListSetMap のようなコレクションでケースクラスを使いやすくします。また、オブジェクトを参照 ID の代わりに、それらの内容に基づいて比較できるようになります。

    new Person("Noel", "Welsh").equals(new Person("Noel", "Welsh"))
    // res3: Boolean = true
    
    new Person("Noel", "Welsh") == new Person("Noel", "Welsh")
    // res4: Boolean = true
    
  4. copy メソッドcopy メソッドは、現在のオブジェクトと同じフィールド値を伴う新しいオブジェクトを生成します。

    dave.copy()
    // res5: Person = Person(Dave,Gurnell)
    

    copy メソッドは、現在のオブジェクトの代わりに、クラスの新しいオブジェクトを生成して返すことに注意してください。

    実のところ、copy メソッドは、それぞれのコンストラクター引数と一致する任意の引数を受け取ります。引数が指定されれば、新しいオブジェクトは、既存のオブジェクトに存在する値の代わりにその値を使用します。これは、1つ以上のフィールド値を変更してオブジェクトをコピーするときに、キーワード引数と一緒に使用するのが理想的です。

    dave.copy(firstName = "Dave2")
    // res6: Person = Person(Dave2,Gurnell)
    
    dave.copy(lastName = "Gurnell2")
    // res7: Person = Person(Dave,Gurnell2)
    

    :information_source: 値と参照の等価性

    Scala の == 演算子は Java のものとは異なります。それは、参照 ID を値として比較する代わりに equals に委譲します。

    Scala は、Java の == と同じ振る舞いをする eq と呼ばれる演算子を持ちます。しかしながら、それをアプリケーションコードで使用することはめったにありません。

    new Person("Noel", "Welsh") eq (new Person("Noel", "Welsh"))
    // res8: Boolean = false
    
    dave eq dave
    // res9: Boolean = true
    

  5. ケースクラスは java.io.Serializablescala.Product という2つのトレイトを実装します。どちらも直接使用されることはありません。後者は、ケースクラスの名前とフィールド数を調べるためのメソッドを提供します。

ケースクラスコンパニオンオブジェクトの機能

コンパニオンオブジェクトは、クラスのコンストラクターと同じ引数を持つ apply メソッドを含みます。Scala プログラマーは、new を省略できる簡潔さのために、コンストラクターより apply メソッドを好む傾向があり、コンストラクターが式の中で読み易くなるようにします。

Person("Dave", "Gurnell") == Person("Noel", "Welsh")
// res10: Boolean = false

Person("Dave", "Gurnell") == Person("Dave", "Gurnell")
// res11: Boolean = true

最後に、コンパニオンオブジェクトは、パターンマッチング (pattern matching) に使用する抽出子パターン (extractor pattern) を実装するコードも含みます。本章で後ほど見ていきます。


:information_source: ケースクラス宣言文法

ケースクラスを宣言するための文法は、

case class Name(parameter: type, ...) {
  declarationOrExpression ...
}

です。ここで、

  • Name はケースクラスの名前
  • parameter はコンストラクター引数として与えられた名前(オプション)
  • type はコンストラクター引数の型
  • declarationOrExpression は宣言か式(オプション)

とします。


ケースオブジェクト

最後の覚え書きです。コンストラクター引数なしでケースクラスを定義しているものをみつけたら、その代わりにケースオブジェクト (case object) を定義できます。ケースオブジェクトは、単に通常のシングルトンオブジェクトのように定義されますが、もっと意味のある toString メソッドを持ち、ProductSerializable トレイトを継承しています。

case object Citizen {
  def firstName = "John"
  def lastName  = "Doe"
  def name = firstName + " " + lastName
}
Citizen.toString
// res12: String = Citizen

覚えておいてほしいこと

ケースクラスは Scala データ型の真髄です。それを使って、学んで、好きになってください。

ケースクラスを宣言するための文法は、case を付与すること以外はクラスを宣言するためのものと同じです。

case class Name(parameter: type, ...) {
  declarationOrExpression ...
}

ケースクラスは、タイピングの手間を省く、たくさんの自動生成されたメソッドと機能を持ちます。関連するメソッドを実装することによって、その動作を個別にオーバーライドできます。

Scala 2.10 以前は、0〜22のフィールドを含むケースクラスしか定義できませんでした。Scala 2.11 で、任意サイズのケースクラスを定義する能力を獲得しました。

演習

ケースキャット

Cat が色とエサを String で持っていることを思い出してください。Cat を表現するケースクラスを定義しましょう。

解答(クリックして表示)

もうひとつの簡単な指の運動です。

case class Cat(colour: String, food: String)

ロジャー・イーバートは言う……

良くない映画は長すぎるが、悪くない映画は十分に短い。

コードについては必ずしも同じことを言えませんが、この場合、DirectorFilm をケースクラスに変換することによって、多くの定型文を取り除くことができます。この変換を実行し、どのコードを削除できるか試してみましょう。

解答(クリックして表示)

ケースクラスは copy メソッドと apply メソッドを提供し、各コンストラクター引数の前にある val を記述する必要性を取り除きます。最終的なコードベースは下記のようになります。

case class Director(firstName: String, lastName: String, yearOfBirth: Int) {
  def name: String =
    s"$firstName $lastName"
}

object Director {
  def older(director1: Director, director2: Director): Director =
    if (director1.yearOfBirth < director2.yearOfBirth) director1 else director2
}

case class Film(
  name: String,
  yearOfRelease: Int,
  imdbRating: Double,
  director: Director) {

  def directorsAge =
    yearOfRelease - director.yearOfBirth

  def isDirectedBy(director: Director) =
    this.director == director
}

object Film {
  def newer(film1: Film, film2: Film): Film =
    if (film1.yearOfRelease < film2.yearOfRelease) film1 else film2

  def highestRating(film1: Film, film2: Film): Double = {
    val rating1 = film1.imdbRating
    val rating2 = film2.imdbRating
    if (rating1 > rating2) rating1 else rating2
  }

  def oldestDirectorAtTheTime(film1: Film, film2: Film): Director =
    if (film1.directorsAge > film2.directorsAge) film1.director else film2.director
}

このコードはかなり短くなっただけでなく、equals メソッドや toString メソッド、後ほどの演習の準備にもなるパターンマッチング機能を提供します。

ケースクラスカウンター

ケースクラスとして Counter を再実装し、適切なところで copy を使用してください。さらに、0 をデフォルト値として count を初期化しましょう。

解答(クリックして表示)
case class Counter(count: Int = 0) {
  def dec = copy(count = count - 1)
  def inc = copy(count = count + 1)
}

これは、ほとんど引っ掛け問題です。前回の実装と、ほんの少しの違いしかありません。しかしながら、無料で得られている追加の機能に注目してください。

Counter(0) // `new` なしでオブジェクトを構築
// res16: Counter = Counter(0)

Counter().inc // 印字で `count` の値を表示
// res17: Counter = Counter(1)

Counter().inc.dec == Counter().dec.inc // 意味のある等価性を検証
// res18: Boolean = true

適用、適用、適用

ケースクラスにコンパニオンオブジェクトを定義すると何が起こるでしょうか?見てみましょう。

前節の Person クラスを題材に、ケースクラスへの変換を試してみましょう。(ヒント:コードは上にあります。)代替 apply メソッドを伴うコンパニオンオブジェクトを依然として持っていることを確認してください。

解答(クリックして表示)

こちらがコードです。

case class Person(firstName: String, lastName: String) {
  def name = firstName + " " + lastName
}

object Person {
  def apply(name: String): Person = {
    val parts = name.split(" ")
    apply(parts(0), parts(1))
  }
}

Person のためにコンパニオンオブジェクトを定義しているにも関わらず、依然として Scala のケースクラスのコードは期待どおりに動作します。自動生成されたメソッドを定義したコンパニオンオブジェクトに追加するので、ひとつのコンパイル単位にクラスとコンパニオンオブジェクトを配置する必要があります。

これは、最終的に apply メソッドのオーバーロードによって、2つの型シグネチャをコンパニオンオブジェクトが持つこと意味します。

def apply(name: String): Person =
  // etc...

def apply(firstName: String, lastName: String): Person =
  // etc...

パターンマッチング

これまでは、メソッドを呼び出したり、フィールドにアクセスしたりしてオブジェクトを作用させてきました。ケースクラスを使用すると、パターンマッチング (pattern matching) を通じた別の方法で作用させることができます。

パターンマッチングは、データの「形」に応じて式を評価できるように拡張した if 式のようなものです。前の例で見た Person ケースクラスを思い出してください。

case class Person(firstName: String, lastName: String)

反乱軍のメンバーを探している Stormtrooper を実装したいとしましょう。下記のようにパターンマッチングを使用することができます。

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"
    }
}

パターンの文法 Person("Luke", "Skywalker") は、パターンマッチングするオブジェクトを構築するための文法 Person("Luke", "Skywalker") と一致していることに注意してください。

使用例はこちらです。

Stormtrooper.inspect(Person("Noel", "Welsh"))
// res0: String = Move along, Noel

Stormtrooper.inspect(Person("Han", "Solo"))
// res1: String = Stop, rebel scum!

:information_source: パターンマッチング文法

パターンマッチング式の文法は、

expr0 match {
  case pattern1 => expr1
  case pattern2 => expr2
  ...
}

です。ここで、

  • expr0 はパターンマッチングする値に評価される式
  • pattern1pattern2 などは値に対して順番に検証されるパターンもしくはガード (guard)
  • expr1expr2 などは式で、最初にマッチングしたパターンの右辺式が評価される2

とします。パターンマッチング自体は式であるため、マッチングしたパターンの右辺式の値に評価されます。

パターン文法

Scala は、パターンやガードを記述するための表現力豊かな文法を持っています。ケースクラスの場合、パターン文法はコンストラクター文法と一致します。データを取り上げてみましょう。

Person("Noel", "Welsh")
// res2: Person = Person(Noel,Welsh)

Person 型に対してマッチするパターンは、下記のように記述されます。

Person(pat0, pat1)

ここで、pat0pat1 は、それぞれ firstNamelastName に対してマッチするパターンです。pat0pat1 の位置に使用できるパターンは4つあります。

  1. 名前。それは、その位置にある任意の値にマッチし、与えられた名前に束縛されます。例えば、パターン Person(first, last) は、名前 first に値 "Noel" を、名前 last に値 "Welsh" を束縛します。

  2. アンダースコア (_)。それは、任意の値にマッチし、その値を無視します。例えば、ストームトルーパーが一般市民の名についてのみ気にするのであれば、lastName の値を名前に束縛することを避け、単に Person(first, _) と書くことができます。

  3. リテラル。それは、単にリテラルが表現する値に首尾よくマッチします。例えば、パターン Person("Han", "Solo") は、名が "Han" で、姓が "Solo" である Person にマッチするというわけです。

  4. 同様のコンストラクタースタイル文法を使用している別のケースクラス。(訳注:ケースクラスを入れ子にできるということです。本節の演習に例があります。)

パターンマッチングでできることは他にもたくさんあり、パターンマッチングは拡張可能であることも覚えておいてください。後ほどの節でそれらの機能を見ていきます。

覚えておいてほしいこと

ケースクラスは、パターンマッチングと呼ばれる相互作用の新しい形を可能にします。パターンマッチングでは、ケースクラスによって分析し、ケースクラスに含まれる内容に応じて異なる式を評価することができます。

パターンマッチングのための文法は、

expr0 match {
  case pattern1 => expr1
  case pattern2 => expr2
  ...
}

です。パターンは、下記のいずれかになります。

  1. 名前。その名前に任意の値が束縛される
  2. アンダースコア。任意の値がマッチし、その値は無視される
  3. リテラル。リテラルが意味する値にマッチする
  4. ケースクラスによるコンストラクタースタイルパターン

演習

猫にエサをあげる

willServe メソッドを伴う ChipShop オブジェクトを定義してください。このメソッドは Cat を受け取り、猫の好きなエサがカリカリ (chips) であれば true を、そうでなければ false を返します。パターンマッチングを使用しましょう。

解答(クリックして表示)

問題文が示唆しているスケルトンを記述することから始めましょう。

case class Cat(name: String, colour: String, food: String)
object ChipShop {
  def willServe(cat: Cat): Boolean =
    cat match {
      case Cat(???, ???, ???) => ???
    }
}

返却型は Boolean なので、少なくとも2つのケース、ひとつは true、もうひとつは false が必要であることがわかります。演習の文章は、それがカリカリが好きな猫とそれ以外の猫であることを示しています。これをリテラルパターンと _ パターンで実装することができます。

object ChipShop {
  def willServe(cat: Cat): Boolean =
    cat match {
      case Cat(_, _, "Chips") => true
      case Cat(_, _, _) => false
    }
}

私の芝生から出ていけ!

この演習では、映画評論家である私の父のシミュレーターを書こうと思います。それはとても単純で、クリント・イーストウッドが監督した映画はどれでも10.0、ジョン・マクティアナンが監督した映画はどれでも7.0、ほかの映画はどれでも3.0に評価されます。Film を受け取り、Double を返す rate メソッドを伴う Dad と呼ばれるオブジェクトを実装してください。パターンマッチングを使用しましょう。

解答(クリックして表示)
object Dad {
  def rate(film: Film): Double =
    film match {
      case Film(_, _, _, Director("Clint", "Eastwood", _)) => 10.0
      case Film(_, _, _, Director("John", "McTiernan", _)) => 7.0
      case _ => 3.0
    }
}

この場合、パターンマッチングはいささか冗長になります。後ほど、定数パターン (constant pattern) と呼ばれる特定の値にマッチするパターンマッチングを使用する方法を学びます。

まとめ

本章ではクラスを探求しました。そのクラスが、オブジェクトを超えた抽象化を可能にすることも見てきました。それによって、共通のプロパティを共有し、共通の型を持つオブジェクトを定義できます。

コンパニオンオブジェクトについてもまた見てきました。それは、Scala において、補助コンストラクターや、クラスに属さないユーティリティメソッドを定義するために使用されます。

最後に、ケースクラスを紹介しました。それは、ボイラープレートコードを大いに削減し、メソッド呼び出しに加えて、パターンマッチングというオブジェクトとの新しい相互作用の方法を可能にしました。


  1. 実際には AnyVal の派生型を定義でき、それは値クラスとして知られています。これらはいくつかの特殊な状況で有用なのですが、ここでは議論しません。 

  2. 実際のところ、パターンは逐次的な検証より効率的な形にコンパイルされますが、それが意味するところは変わりません。 

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2