Scala2.11から、unapplyメソッドの戻り値型の仕様が拡張されます

この変更
https://github.com/scala/scala/pull/2848
すでに2.11.0-M5に入っているので試せます。unapplyとは、パターンマッチの際に使われる特別なメソッド名です。
一言でいうと
「今まではscala.Option型*1を返さないといけなかったが、getとisEmptyというメソッドを持っていればなんでもいい」
という仕様になりました。scala.Option自体には、もともとisEmptyとgetが存在していたので、基本的には2.10以前とソースコードは互換性あるはずです。


この変更の目的のほとんどは、パフォーマンス面でしょう。「unapplyのメソッドから返すためだけに毎回Optionに包む」という余計なオーバヘッドを、今回の変更と、「2.10から入ったvalue class」を併用して避けるためです。また、Option[AnyVal]の場合は、*2Intからjava.lang.Integerなどの何らかのAnyRef型にboxingされ、さらにOptionに包まれるという、2重のオーバヘッドがかかっています。


さて、それらを防ぐための実装ですが、value classの制約をちゃんと理解してないとハマります。わりとScalaに詳しい人*3でも勘違いしてました↓*4

http://hseeberger.github.io/blog/2013/10/04/name-based-extractors-in-scala-2-dot-11/

まず、Option[A]で、AがAnyRefの場合を考えましょう。以下のように実装したら間違いです、value classの意味がありません

package opt

sealed trait Opt[+A] extends Any{
  def get: A
  def isEmpty: Boolean
}

class Some[+A](val get: A) extends AnyVal with Opt[A]{
  def isEmpty = false
}

object None extends Opt[Nothing]{
  def isEmpty = true
  def get = throw new UnsupportedOperationException("None.get")
}


使う側↓

object NonEmptyStr{
  def unapply(str: String): Opt[String] =
    if(str.isEmpty) opt.None
    else new opt.Some(str)
}

object Main extends App{
  "a" match {
    case NonEmptyStr(s) => println("non empty")
    case _ => println("empty")                                   
  }
}

Opt型がAny*5で、Some型がAnyValを継承してvalue classにして、unapplyでOpt型を返しても、value classの意味はありません!このあたり読んでください。
正しくは、以下のように実装します

package opt

class Opt[+A](val get: A) extends AnyVal{
  def isEmpty: Boolean = get == null
} 

object Opt{
  val None: Opt[Null] = new Opt(null)
}
object NonEmptyStr{
  def unapply(str: String): Opt[String] =
    if(str.isEmpty) Opt.None
    else new Opt(str)
}

object Main extends App{
  "a" match {
    case NonEmptyStr(s) => println("non empty")
    case _ => println("empty")                                 
  }
}

さて、AnyRefの場合は上記のようにisEmptyをdef isEmpty: Boolean = get == nullと定義すればいいです。が、AnyValの場合はどうすればいいでしょうか?これを実装したpaulpさんのpull requestのコメント欄には以下のような実装が載っていました

final class OptInt(val x: Int) extends AnyVal {
  def get: Int = x
  def isEmpty = x == Int.MinValue // or whatever is appropriate
}

最初意味がわかりませんでした。なぜこんな実装にしないといけないかというと、AnyValの場合はnullが使えない*6ので、どれか一つIntの値を犠牲にしないといけません!
上記では、例としてInt.MinValueを犠牲にするということです。つまりこのOptInt型で表現できる値は
「"Int.MinValue + 1" から Int.MaxValue まで」
となり、厳密にIntがとりうる範囲すべてを表現できない、ということです。



このように、たしかに工夫すればメモリ割り当てのオーバヘッドを防げる素晴らしい機能ですが、仕様や制限を把握していないとハマるし、また、上記のように内部実装も多少残念にならざるを得ません。
デメリットというか、注意点をまとめると

  • trait Opt extends Any; class Some extends AnyVal with Opt;としては意味が無い
  • AnyValの場合*7はさらに"どれか一つの値を犠牲にする"ということが必須
  • また、specializedアノテーションとvalue classは併用できないので、上記のようなIntOpt型は、(もしLongOptなどが欲しいならば)AnyValの種類ごとにそれぞれ作る必要がある
  • よって、抽象度が下がったり*8 *9、あきらかな制限が増えたりするので、「現在scala.Optionを使ってるところをすべておきかえる」とはいかずに、それなりに使い分けするべきだと思う
  • つまりパフォーマンス問題ないなら、「この機能全く使わずに、今までどおりscala.Optionをそのまま使う」でいいと思う

*1:もしくはBoolean

*2:scala.Optionはspecializedアノテーションはついていないので

*3:というか、typesafe社の人・・・

*4:eed3si9nさんがツッコんでくれて、もう修正済みです

*5:正式にはユニバーサルtraitだっけ?

*6:nullを許容するようにしたらIntからIntegerにboxingされてしまう。それも防ぐのが目的

*7:IntからIntegerへのboxingも避けたい場合

*8:scala.Optionと、AnyRef用のOptionと、Int用のOptionと、何種類も扱わないといけなくなる?

*9:このunapplyの仕様が、というより、value classに制限が多い。たとえば、scalazのMonadを実装しようとしても「type erasureしたあとのシグネチャがぶつかるから、実装できないよ!」とコンパイラに言われたりとか