CLOVER🍀

That was when it all began.

Scala 2.10.0 Implicit Classes

昨日に続いて、今日はSIP-13 Implicit Classesです。
http://docs.scala-lang.org/sips/pending/implicit-classes.html

今までImplicit Conversionを使う上で面倒な定義を、楽にしてくれるもののようで。これまでは、確かに

  • 変換するためのメソッド定義
  • 変換先の型の定義

の両方が必要でしたからね。

ドキュメントには、

implicit class RichInt(n: Int) extends Ordered[Int] {
  def min(m: Int): Int = if (n <= m) n else m
  ...
}

class RichInt(n: Int) extends Ordered[Int] {
  def min(m: Int): Int = if (n <= m) n else m
  ...
}
implicit final def RichInt(n: Int): RichInt = new RichInt(n)

コンパイラが変換するんだよ!と書いてます。

まあ、ちょっと試してみましょう。

例えば、こんなソースをImplicit Classesを使って書き直すと
ImplicitConversionsExample.scala

// 以下のimport文を入れないと、Scala 2.10.0では警告が出ます
import scala.language.implicitConversions

class IntWrapper(val underlying: Int) {
  def twice: Int = underlying * 2
}

object IntWrapper {
  implicit def int2wrapper(i: Int): IntWrapper =
    new IntWrapper(i)
}

import IntWrapper._
object ImplicitConversionsExample {
  def main(args: Array[String]): Unit = {
    println("twiced = " + 2.twice)
  }
}

こうなるということ?
ImplicitClasses.scala

implicit class IntWrapper(val underlying: Int) {
  def twice: Int = underlying * 2
}

object ImplicitClasses {
  def main(args: Array[String]): Unit = {
    println("twiced = " + (2.twice))
  }
}

ちょっとコンパイル

$ scalac ImplicitClasses.scala 
ImplicitClasses.scala:1: error: `implicit' modifier cannot be used for top-level objects
implicit class IntWrapper(val underlying: Int) {
               ^
one error found

お…?トップレベルでは、「implicit」修飾詞は使えないと仰ってる?

ちょっと考えて

implicit class RichInt(n: Int) extends Ordered[Int]

class RichInt(n: Int) extends Ordered[Int]
implicit final def RichInt(n: Int): RichInt = new RichInt(n)

となるんだから…

こういうこと?

object ImplicitClasses {
  implicit class IntWrapper(val underlying: Int) {
    def twice: Int = underlying * 2
  }

  def main(args: Array[String]): Unit = {
    println("twiced = " + (2.twice))
  }
}
$ scalac ImplicitClasses.scala 
$ scala ImplicitClasses
twiced = 4

コンパイル通った、そして動いた。

ふーん、今まで

class IntWrapper(val underlying: Int) {
  def twice: Int = underlying * 2
}

object IntWrapper {
  implicit def int2wrapper(i: Int): IntWrapper =
    new IntWrapper(i)
}

import IntWrapper._

みたいにimportで引っ張ってきた部分ってどうするんだろ?と思ってましたけど、そうなるんですか…。

ということは、何かしらのobjectのメンバーとしてImplicit Classを定義して、そのオブジェクトのメンバーとしてimportすればいいのかな?
ICwithPackage.scala

package foo {
  object IC {
    implicit class IntWrapper(val underlying: Int) {
      def twice: Int = underlying * 2
    }
  }

  import foo.IC.IntWrapper
  package bar {
    class Bar {
      def twiceTwice(n: Int): Int = n.twice.twice
    }
  }
}

import foo.bar.Bar
package fuga {
  object Main {
    def main(args: Array[String]): Unit = 
      println("twiceTwiced = " + new Bar().twiceTwice(2))
  }
}

コンパイル、実行。

$ scalac ICwithPackage.scala
$ scala fuga.Main
twiceTwiced = 8

動きましたね。

import foo.IC._

みたいに、全部引っ張ってこなくてもいいみたい?

あとは気になるのは…

Value Classとの組み合わせ

下記のように、Implicit ClassとValue Classを組み合わせることで、Implicit Conversionをオーバーヘッドなく使えるんだそうで。

object ImplicitClasses {
  implicit class StringWrapper(val underlying: String) extends AnyVal {
    def star: String = s"***$underlying***"
  }

  def main(args: Array[String]): Unit = {
    println("Hello World".star)
  }
}

アノテーションと一緒に使う

こんな感じにImplicit Classにアノテーションを付与すると

    @bar
    implicit class Foo(n: Int)

これは、以下の構文糖衣らしいです。

    @bar implicit def Foo(n: Int): Foo = new Foo(n)
    @bar class Foo(n:Int)

ということは…
Bar.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bar { }

みたいなファイルを作って、
WithAnno.scala

object WithAnno {
  @Bar
  implicit class IntWrapper(val underlying: Int) {
    def twice: Int = underlying * 2
  }

  def main(args: Array[String]): Unit =
    println(2.twice)
}

みたいに付けてコンパイルすると…

$ scalac WithAnno.scala

コンパイルエラーにならない(笑)。

しかも、展開されている…。

$ scalac -Xprint:jvm WithAnno.scala 
[[syntax trees at end of                       jvm]] // WithAnno.scala
package <empty> {
  object WithAnno extends Object {
    @Bar implicit <synthetic> def IntWrapper(underlying: Int): WithAnno$IntWrapper = new WithAnno$IntWrapper(underlying);
    def main(args: Array[String]): Unit = scala.this.Predef.println(scala.Int.box(WithAnno.this.IntWrapper(2).twice()));
    def <init>(): WithAnno.type = {
      WithAnno.super.<init>();
      ()
    }
  };
  @Bar implicit class WithAnno$IntWrapper extends Object {
    <paramaccessor> private[this] val underlying: Int = _;
    <stable> <accessor> <paramaccessor> def underlying(): Int = WithAnno$IntWrapper.this.underlying;
    def twice(): Int = WithAnno$IntWrapper.this.underlying().*(2);
    def <init>(underlying: Int): WithAnno$IntWrapper = {
      WithAnno$IntWrapper.this.underlying = underlying;
      WithAnno$IntWrapper.super.<init>();
      ()
    }
  }
}

他にいろいろ試してみましたけど、コンパイルエラーになりませんね。

@Target({ElementType.TYPE, ElementType.METHOD})

と書かないとエラーになるんじゃないかなぁと思ったんですが、どうやらScalaコンパイラは(Javaで書いた)アノテーションの配置可能な場所まではチェックはしてないようですね…。