kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scala 2.10.0 M3の新機能を試してみる(3) - SIP-15 - Value Classes

Scala 2.10.0 M3の新機能を試してみる記事第三段。今度はSIP-15 - Value classesです。

この機能は、一言で言うと、従来ユーザが継承することができなかったAnyValを継承したクラスを作ることができるようになる機能です。この機能の主な意義は、特定の状況において、オブジェクト確保に伴うオーバーヘッドを0にすることです。

たとえば、implicit conversionによるPimp My Libraryパターンを考えてみましょう。前回の記事でimplicit classを使いましたが、

implicit class RichString(self: String) {
  def display(): Unit = println(self)
}
"foo".display

このとき、"foo".displayの部分は、実際には

new RichString("foo").display

のように展開され、displayメソッドを呼ぶたびにオブジェクトが確保されてしまいます。実際には、JDK1.6のuN(Nが何だったかは忘れました)から有効になったエスケープ解析が効く場合、このようなオブジェクトの確保は消去され、スタックに割り付けられる事も多いです。しかし、エスケープ解析があるゆる場合に機能する事が保障されているわけでは無いので、必ずしもそれに頼ることはできません。

そこで出てくるのがValue classesです。上述のRichStringにextends AnyValを付けるだけで、オブジェクト確保のオーバヘッドが0になる優れもの機能です。というわけで、早速試してみましょう。

object ValueClasses {
  implicit class RichString(self: String) extends AnyVal {
    def display(): Unit = println(self)
  }
  def main(args: Array[String]) {
    "foo".display
  }
}

コンパイル結果:

error:
     while compiling:  ValueClasses.scala
       current phase:  typer
     library version:  version 2.10.0-M3
    compiler version:  version 2.10.0-M3
  reconstructed args:

uncaught exception during compilation: java.util.NoSuchElementException
error: java.util.NoSuchElementException: key not found: value RichString
        at scala.collection.MapLike$class.default(MapLike.scala:228)
        at scala.collection.AbstractMap.default(Map.scala:57)
        at scala.collection.mutable.HashMap.apply(HashMap.scala:63)
        at scala.tools.nsc.typechecker.MethodSynthesis$MethodSynth$class.addDeri
vedTrees(MethodSynthesis.scala:237)


大変残念なことにコンパイラが落ちてしまいました。色々試した結果、Implicit classesとValue classesを組み合わせるとコンパイラが落ちる*1らしい事がわかったため、Implicit classesを使わないように書き換えてみました。以下が書き換え後のコードです。

object ValueClasses {
  class RichString(val self: String) extends AnyVal {
    def display(): Unit = println(self)
  }
  implicit def enrichString(self: String): RichString = new RichString(self)
  def main(args: Array[String]) {
    "foo".display
  }
}

さて、このコードをコンパイルしたものを逆アセンブルしてみましょう。

> scalac ValueClasses.scala

> javap -c ValueClasses$
public final class ValueClasses$ {
  public static final ValueClasses$ MODULE$;

  public static {};
    Code:
       0: new           #9                  // class ValueClasses$
       3: invokespecial #12                 // Method "<init>":()V
       6: return

  public java.lang.String enrichString(java.lang.String);
    Code:
       0: aload_1
       1: areturn

  public void main(java.lang.String[]);
    Code:
       0: getstatic     #26                 // Field ValueClasses$RichString$.MO
DULE$:LValueClasses$RichString$;
       3: aload_0
       4: ldc           #28                 // String foo
       6: invokevirtual #30                 // Method enrichString:(Ljava/lang/S
tring;)Ljava/lang/String;
       9: invokevirtual #34                 // Method ValueClasses$RichString$.e
xtension$display:(Ljava/lang/String;)V
      12: return
}

このコードで注目すべきは、enrichStringがRichStringではなく、String型の自分自身のインスタンスを返すようになっている点です。これは、暗黙にオブジェクトを確保することなくimplicit conversionを使えることを意味しています。これまでは、エスケープ解析が期待できない場合、implicit conversionを大量に使うとオブジェクト確保のオーバーヘッドがあったわけですが、Value classesを使うことによって、そのオーバーヘッドを実質的に無視することができるようになります。

Implicit classesとValue classesを組み合わせたときに落ちるのは単なるコンパイラのバグでしょうから、10.0.finalが出るまでには改善されると期待して良いでしょう。これで、性能の問題を気にすることなくimplicit conversionによるPimp My Libraryパターンを思う存分(もちろん使いすぎには可読性上の問題がありますが)使うことができるようになります。

*1:id:kxbmap さん情報:このバグはmasterブランチでは既に修正済みのようです。 cf. https://github.com/scala/scala/commit/5c84dc85bf1a19aedf1ad96c9b8007c1368dd79f