無限大な夢のあと

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

ドワンゴの新卒エンジニア向けの研修資料でScalaに入門 その4(オブジェクト)

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

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

学んだことをとにかく走り書きしていきます。
【オブジェクト】
・オブジェクトに関して

Scalaでは、全ての値がオブジェクトです。また、全てのメソッドは何らかのオブジェクトに所属しています。そのため、Javaのようにクラスに属するstaticフィールドやstaticメソッドといったものを作成することができません。その代わりというと若干語弊があるのですが、objectキーワードによって、同じ名前のシングルトンオブジェクトをグローバルな名前空間に1つ定義することができます。objectキーワードによって定義したオブジェクトもオブジェクトであるため、メソッドやフィールドを定義することができます。

全ての値がオブジェクトであるのは、Rubyなどと一緒なので、よしとするが、staticフィールドやstaticメソッドが作成できないのか。
staticの代わり?にobjectキーワードで作成されたシングルトンオブジェクト内に定義するイメージか。
・object構文の主な用途

object構文の主な用途としては、

・ユーティリティメソッドやグローバルな状態の置き場所(Javaで言うstaticメソッドやフィールド)
・オブジェクトのファクトリメソッド
・Singletonパターン
3つが挙げられます。とはいえ、Singletonパターンを実現するために使われることはほとんどなく、もっぱら最初の 2つの用途で使われます。

objectの基本構文はクラスとほとんど同じで、

object オブジェクト名 extends クラス名 with トレイト名1 with トレイト名2 ... {
  本体
}

のようになります。Scalaでは標準でPredefというobjectが定義・インポートされており、これは最初の使い方に当てはまります。たとえば、println()などとなにげなく使っていたメソッドも実はPredef objectのメソッドなのです。

Object構文の主な用途が3つというのはなんとなくイメージできた。
Predefオブジェクトの話は、コップ本に書いていたから理解済み。

一方、2番めの使い方について考えてみます。点を表すPointクラスのファクトリを objectで作ろうとすると、次のようになります。applyという名前のメソッドはScala処理系によって特別に扱われ、Point(x)のような記述があった場合で、Point objectにapplyという名前のメソッドが定義されていた場合、Point.apply(x)と解釈されます。これを利用してPoint objectの applyメソッドでオブジェクトを生成するようにすることで、Point(3, 5)のような記述でオブジェクトを生成できるようになります。

scala> class Point(val x:Int, val y:Int)
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.

これは、new Point()で直接Pointオブジェクトを生成するのに比べて、
・クラス(Point)の実装詳細を内部に隠しておける(インタフェースのみを外部に公開する)
・Pointではなく、そのサブクラスのインスタンスを返すことができる
といったメリットがあります。

サブクラスのインスタンスを返すことができるというのが、イマイチまだピンと来ていないが、他は大まかに理解。

なお、上記の記述はケースクラスを用いてもっと簡単に

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

と書けます。ケースクラスは後述するパターンマッチのところでも出てきますが、ここではその使い方については触れません。簡単に言うとケースクラスは、それをつけたクラスのプライマリコンストラクタ全てのフィールドを公開し、equals()・hashCode()・toString()などのオブジェクトの基本的なメソッドを持ったクラスを生成し、また、そのクラスのインスタンスを生成するためのファクトリメソッドを生成するものです。たとえば、 case class Point(x: Int, y: Int)で定義した Point クラスは equals() メソッドを明示的に定義してはいませんが、

Point(1, 2).equals(Point(1, 2))

を評価した値はtrueになります。

ケースクラスにした場合は、Javaでいう自分でクラスを作成した時に作るequals、hashCode、toStringを作ってくれるのか。
これは便利。
フィールドが公開となっているのは、case classでmatch文を使う時にgetter/setterを書いていると冗長になるからかな。

・コンパニオンオブジェクトに関して

クラス名と同じ名前のシングルトンオブジェクトはコンパニオンオブジェクトと呼ばれます。コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っています。たとえば、 weightをprivateにした場合、

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

object Hoge {
  val taro = new Person("Taro", 20, 70)
  println(taro.weight)
}

はNGですが、

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

object Person {
  val taro = new Person("Taro", 20, 70)
  println(taro.weight)
}

はOKです。なお、コンパニオンオブジェクトでも、private[this](そのオブジェクト内からのみアクセス可能)なクラスのメンバーに対してはアクセスできません。単にprivateとした場合、コンパニオンオブジェクトからアクセスできるようになります。

class Point(val x: Int, val y: Int) {
  def +(p: Point): Point = {
    new Point(x + p.x, y + p.y)
  }
  override def toString(): String = "(" + x + ", " + y + ")"
}

上記のような感じでメソッド定義の中から直接コンストラクタ引数を参照できるそうです。