この変更
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したあとのシグネチャがぶつかるから、実装できないよ!」とコンパイラに言われたりとか