Scala で case class と apply() と unapply() の関係がよくわかってなかったので調べたら関数リテラルについても学びがあった話

unapply() のあたりがどうにもよくわからなかったのと、読んでいる Programming Scala で扱っている Scala のバージョンが 2.7 系とかなり古くて、現在の仕様と差があるようなので、調べた。

2015 Nov. 4th 現在 Scala 2.11.6 を基準としている。

constractor フィールド定義

まず、予備知識兼 Scala 大好き糖衣構文について確認。

class Hoge (myName: String, myAge: Int) {
  val name = myName
  val age = myAge
}

class Hoge (val name: String, val age: Int)

のように書ける。これは便利。

apply() / unapply() と case class による自動定義

Scala 関連のドキュメントを読んでいると「case class を定義すると自動的に apply() メソッドと unapply() メソッドが定義されて便利だよ」と書いてあって天下り的に使い方が書いてあることが多いけれど、apply() ってなんだよそんな呼び出しないじゃん〜みたいな人間なので apply() / unapply() を調べた。

普通は上記のように case class がメインに説明してあることが多いけれど、自分は apply() と unapply() 理解してから case class がよく分かるようになった気がする。

apply()

apply() ではファクトリメソッドとかを小奇麗に呼び出せる。

class Hoge private (val name: String)

object Hoge {
  def apply (aName: String) = {
    new Hoge(aName)
  }
}

val hoge = Hoge("Taro")  // Hoge.apply("Taro") と等価
println(hoge.name)

クラスもしくはオブジェクトに apply() メソッドを定義すると、クラス名/オブジェクト名(引数) の形でそのクラス/オブジェクトに定義された apply() メソッドを呼び出すことができる。上の例では、Hogeクラスのコンストラクタを private にして外から呼び出せないようにし、コンパニオンオブジェクトの apply(aName: String) メソッドをファクトリメソッドとして定義し、クラス名(引数)の形式で呼び出している。newキーワードがいらないし、まぁ小奇麗。
apply() の呼び出しが見かけ上隠蔽されているので、自分で書かないかぎり見かけない。

実際、Scala のデータ構造 List や Seq などではコンパニオンオブジェクトに apply() メソッドが定義されていて実際よく使われる。

val piyo = List(1, 2, 3)  // List.apply[Int](1, 2, 3) と等価

※実は Scala における関数リテラルも apply() メソッドただひとつを持つオブジェクトを生成する糖衣構文だったりする。後述。

unapply() では apply() と逆にインスタンスのコンストラクタに利用された(はずの)情報を取り出したり、パターンマッチで入力の値をバラすことができる。(cf. 抽出子)

class Hoge private (val name: String)

object Hoge {
  def apply (aName: String) = {
    new Hoge(aName)
  }
  def unapply (input: Hoge): Option[String] = {
    Option(input.name)
  }
}

val hoge = Hoge("Taro")
val hogeName = hoge match {
  case Hoge(n) => n
  case _ => "John Doe"
}

println(hogeName)

クラスもしくはオブジェクトに unapply() メソッドを定義すると、match式の条件式にクラス名/オブジェクト名(値を束縛する変数) の形でそのクラス/オブジェクトに定義された unapply() メソッドを呼び出すことができ、戻り値を与えられた変数に束縛することができる。上の例では、コンパニオンオブジェクトに unapply(aName: String) メソッドを定義し、match式中でクラス名(値を束縛する変数)の形式で呼び出して、束縛した値を返すようにしている。

unapply() を使うことで、パターンマッチで n に値を束縛できているのがわかる。

わかるけど、これ unapply() の実装を考えるとかなり気持ち悪い。
だって n って書いてあるところそこ引数じゃん。なんでしれっと値束縛されてんの。引数どこから来たんだよ(match式からです)。

実際には

input match {
  case { n = Hoge(input) } => n  // !!イメージのために書いたので正しくない!!
}

なイメージだと思う(イメージのために書いたので動きません)。

この辺が、n に値が代入されるじゃなくて束縛されるって所以なんだろうなぁ。
しかしキモい。

apply() も含めてこんなしちめんどくさいの使うか!?という疑問の答えになっているのが case class になる。これは理解したら感動した。

先述の通り、Scala では case class を定義すると apply() と unapply() が自動定義される。

sealed trait Hoge[+A]
case class Piyo[+A](name: A) extends Hoge[A]
case class Fuga[+A](name: A) extends Hoge[A]

val piyo = Piyo("Taro")  // 自動定義された apply() メソッドが暗黙に呼び出されている

def piyoTaro(meta: Hoge[String]): String = meta match {
  case Piyo(myName) => myName  // 自動定義された unapply() メソッドが呼び出されて、
                               // コンストラクタに渡された name フィールドが myName に束縛される
  case Fuga(myName) => myName
}

println(piyoTaro(piyo))

case class を用いると、上記のコードの通り apply() と unapply() が対の形で自動定義されて、ロジックとしてめっちょ見やすくなる。これがだいたい天下り的に説明してあるけれど、apply() / unapply() を知っているとなにが起こっているかわかるし、case class のありがたさに気づく。

しかもこの case class がすごいのはこれだけでなく、下記のコードをコンパイルしようとするとエラーする。

sealed trait Hoge[+A]
case class Piyo[+A](name: A) extends Hoge[A]
case class Fuga[+A](name: A) extends Hoge[A]

val piyo = Piyo("Taro")

def piyoTaro(meta: Hoge[String]): String = meta match {
  case Piyo(myName) => myName
  // case Fuga(myName) => myName  // Hoge を継承している Fuga についてケースが漏れているのでエラーする
}

println(piyoTaro(piyo))

<console>:17: warning: match may not be exhaustive.
It would fail on the following input: Fuga(_)
              def piyoTaro(meta: Hoge[String]): String = meta match {

このように、case classとseald trait を match 式で利用すると、コンパイラが勝手に継承関係を調べてコンパイル時に漏れが検出できる。便利。すごい。

というわけで、以上が case class と apply() / unapply() の関係と便利さでした。しかし相変わらず Scala は糖衣構文が大好きだなぁ。

参考:
Scalaオブジェクト
Scala 関数型デザイン & プログラミング (インプレス 2015)
Programming Scala

おまけ:Scalaにおける関数リテラル

Scala では (引数) => 関数本体 で関数リテラルが書けるけれど、実はこれも糖衣構文で、実際は apply() メソッドを持つクラスのインスタンスが生成される。こうすることでJVMの上で動くファーストクラスの値としての関数を実現している。

val greeting = (name : String) => println("Hello, " + name)
greeting("Taro")  // greeting.apply("Taro") と等価

この変換の関係で、関数リテラルだとできないことが結構あるので、むやみに使うよりはdefで定義できるメソッドとうまく使い分けたほうがよさそう。

あと、Java8からクラスがメソッドを一つしか持たない場合 Single Abstract Method (SAM) というもので、Scala の apply() と同じように、その唯一のメソッドをクラス名でコールできるようになったみたい。で、Scala 2.12 系ではJava8移行として apply() ではなくSAMで呼び出す格好になる模様。

Scala の関数 – tkawachi Blog にめちゃくちゃ詳しく書いてあった。

広告

Scala で case class と apply() と unapply() の関係がよくわかってなかったので調べたら関数リテラルについても学びがあった話」への2件のフィードバック

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中