Locked History Actions

scala/callLike

メソッド呼び出しのように見えるもの

Scalaの(特殊な?)文法

Scalaにはオブジェクトに対する直接のメソッド呼び出しのように見える構文がある。 これはJavaで言えばnewの無いコンストラクタ呼び出しのようである。

同じような書き方をしても文脈によって動作は異なり、その動作は実際にはupdate,apply,unapplyというメソッドによって定義される。 例えば以下のようなコードを考えてみる。

class CallLike {
  def update(i: Int, j:Int, value: String): Unit = { println("update") }
  def apply(i: Int, j: Int): Unit =  { println("apply") }
  def unapply(value: String) = { println("unapply"); Some(30, 40) }
}
object Main {
  def main(args: Array[String]) {
    val obj = new CallLike();
    obj(1, 2) = "abc"// update
    obj(3, 4) // apply
    val obj(x, y) = "xyz"; // unapply
  }
}

この実行結果は

update
apply
unapply

になる。同じ一つのobjに続けてカッコをつけて引数(及び引数のようなもの)を記述したとしても、文脈によって実際に呼び出されるメソッドが異なる。

  • updateは、右側に代入演算子(?)のついている場合
  • applyは、代入されていない(ように見える)場合
  • unapplyは、変数定義と同時に初期化している(ように見える)場合

もう少し動作確認

本当に期待する通りの動作を行っているのか、確認のために次のように記述してみる。

class CallLike {
  def update(i: Int, j:Int, value: String) = { println("update %d %d %s" format (i, j, value)); 10 }
  def apply(i: Int, j: Int) =  { println("apply %d %d" format (i, j));  20 }
  def unapply(value: String) = { println("unapply %s" format value); Some(30, 40) }
}
object Main {
  def main(args: Array[String]) {
    val obj = new CallLike();
    val updateReturn = obj(1, 2) = "abc"; // update
    println("updateReturn %d" format updateReturn);
    val applyReturn = obj(3, 4) // apply
    println("applyReturn %d" format applyReturn);
    val obj(x, y) = "xyz"; // unapply
    println("unapplyReturn %d %d" format (x, y))
  }
}

この結果は以下。

update 1 2 abc
updateReturn 10
apply 3 4
applyReturn 20
unapply xyz
unapplyReturn 30 40

update

これは読んで字のごとく、オブジェクトを更新することを意図したもの。

class CallLike {
  def update(i: Int, j:Int, value: String): Unit = { println("update") }
  ...
}
...
    obj(1, 2) = "abc"// update

ScalaにはJavaと異なり言語組み込み(?)の配列はなく、例えばArrayも普通のオブジェクトであるが、そのオブジェクトを更新するには、このupdateを使う。このときの呼び出しは、当然ながら「[]」ではなく「()」を使うことになる。

val a = Array(1, 2, 3)
a(1) = 555
... 結果はArray(1, 555, 3)

あるいは、updateを明示的に呼び出しても同じ

a.update(1, 555)

ちなみに、配列の要素を取得する場合は、

val b = a(0)

であるが、これはapplyが呼び出されている(と思う)。つまり、以下でも同じ

val b = a.apply(0)

apply

class CallLike {
  def apply(i: Int, j: Int): Unit =  { println("apply") }
}
...
    obj(3, 4) // apply

上例では、applyの返り値をUnitにしているが、通常は何かを返した方がよい。というよりも、返り値がUnitということは、何らかの副作用があるということだが、これはおそらく不適切。

class CallLike {
  def apply(i: Int, j: Int): Int =  { println("apply"); i + j }
}
...
    val sum = obj(3, 4) // apply

case classのapply

書籍「Scalaスケーラブルプログラミング」の最初の方を読むと、ケースクラスを定義すると、そのクラス名だけで(new 演算子無しに)インスタンスが生成できるようにいっけん思ってしまうのだが(私だけ?)、これはとても紛らわしい。この仕組みもapplyが自動的に定義されているだけのようだ。

どういうことかというと、

case class Foo(i: Int, j: Int)
...
val foo = Foo(1, 2)

としてnew演算子無しにオブジェクトを作成することができるが、実はcase classを定義すると自動的にそのコンパニオンオブジェクトが作られ、その中にapplyがコンストラクタ引数と同じシグニチャのapplyメソッドが作成されているだけなのである(たぶん)。つまり、こんな感じ。

class Foo(i: Int, j: Int)
object Foo {
  def apply(i: Int, j:Int) = new Foo(i, j)
}

つまり、Foo(1, 2)と、いかにもnew無しにコンストラクタを呼び出しているように見えるのは、 実は単にFooというシングルトンオブジェクト(シングルトンオブジェクトもオブジェクトのうち)のapplyメソッドを呼び出している だけなのである。

unapply

class CallLike {
  def unapply(value: String) = { println("unapply"); Some(30, 40) }
}
...
    val obj(x, y) = "xyz"; // unapply

unapplyは「抽出メソッド」とも呼ばれ、引数として与えられたオブジェクトの中身を抽出する、ということになっているが、 それが強制的に行われてしまうわけではなく、「そのような意味で使いなさい」というガイドラインに過ぎない(まぁ、これには従った方がよい)。 実際、上例では与えられた"xyz"という文字列からは何も抽出せず、単に30, 40を返しているだけであるが、これはだめ。

case classのunapply

applyと同様にunapplyもcase classを定義すると自動的に作成されるようだ。つまりこんな感じ(だと思う)。

class Foo(i: Int, j: Int)
object Foo {
  def apply(i: Int, j:Int) = new Foo(i, j)
  def unapply(f: Foo) = Some(f.i, f.j); // ホントは引数がFooじゃなかったらNoneを返すようにする。
}

このおかげで以下のように記述できる。

val foo = Foo(100, 200)
val Foo(x, y) = foo

// xは100、yは200になる。

これもapplyと同様、Fooというシングルトンオブジェクトのunapplyというメソッドを呼び出しているだけである。

unapplyが適用できない場合はどうなるか

class CallLike {
  def unapply(value: String): Option[(Int, Int)] = None
}
...
    val obj(x, y) = "xyz";

この場合にはMatchErrorが発生する。