Scalaのimplicit(暗黙)入門
暗黙の型変換(implicit conversions)
Active Support の再発明
これを使うとRuby on Rails の Active Support みたいな事ができるようになります。
Ruby on Rails Guides: Active Support Core Extensions
d = Date.current # => Mon, 09 Aug 2010 d + 1.year # => Tue, 09 Aug 2011 d - 3.hours # => Sun, 08 Aug 2010 21:00:00 UTC +00:00
簡単ではありますが、これを暗黙の型変換を使って Scala でも実装してみます。
implicit をつけて定義したメソッドで型の変換を行うのがポイントです。
import org.joda.time._ // インタフェースを模倣しているだけ object Date { def current: DateTime = new DateTime() } // DateTime型を型変換 class DateTimeWithActiveSupport(dt: DateTime) { def +(period: Period): DateTime = dt.plus(period) def -(period: Period): DateTime = dt.minus(period) } // メソッド名はユニークにする implicit def convertDateTime(dt: DateTime) = new DateTimeWithActiveSupport(dt) // Int型を型変換 class SyntaxException extends RuntimeException class PeriodWithActiveSupport(intValue: Int) { def year: Period = intValue match { case 1 => Period.years(intValue) case _ => throw new SyntaxException } def years: Period = Period.years(intValue) def hour: Period = intValue match { case 1 => Period.hours(intValue) case _ => throw new SyntaxException } def hours: Period = Period.hours(intValue) } // メソッド名はユニークにする implicit def convertPeriod(intValue: Int) = new PeriodWithActiveSupport(intValue) val d = Date.current d + 1.year d - 3.hours
実行すると以下のように正常に動作します。
$ scala -cp joda-time-*.jar scala> val d = Date.current d: org.joda.time.DateTime = 2011-05-02T15:38:12.412+09:00 scala> d + 1.year res13: org.joda.time.DateTime = 2012-05-02T15:38:12.412+09:00 scala> d - 3.hours res14: org.joda.time.DateTime = 2011-05-02T12:38:12.412+09:00
実際には開発していくときは、こんな感じでまとめたほうが import しやすそうです。
object DateTimeImports { class DateTimeWithActiveSupport(dt: DateTime) { def +(period: Period): DateTime = dt.plus(period) def -(period: Period): DateTime = dt.minus(period) } implicit def convertDateTime(dt: DateTime) = new DateTimeWithActiveSupport(dt) } object PeriodImports { ... } import DateTimeImports._ import PeriodImports._ val d = Date.current d + 1.year d - 3.hours
Twitter 社が公開している Scala の util にも同じようなモジュールがあります。
util/util-core/src/main/scala/com/twitter/conversions/time.scala at master · twitter/util · GitHub
Java から Scala への自動変換
scala.collection に暗黙の型変換を使った Java クラスから Scala クラスへの自動変換が用意されています。
Java のライブラリを使って Scala を書くときには頻繁に利用します。
import collection.JavaConversions._ val javaList = new java.util.ArrayList[String]() javaList.add("aaa") javaList.add("bbb") javaList foreach println
「import collection.JavaConversions._」がないと以下のように当然のごとくエラーになります。
scala> javaList foreach println <console>:7: error: value foreach is not a member of java.util.ArrayList[String] javaList foreach (println) ^
類似の「collection.JavaConverters._」は「asScala」「asJava」のような変換メソッドを生やす働きをします。
import collection.JavaConverters._ val javaList = new java.util.ArrayList[String]() javaList.add("aaa") javaList.add("bbb") javaList foreach println // error: value foreach is not a member of java.util.ArrayList[String] javaList.asScala foreach println List(1,2,3).asJava // java.util.List[Int] = [1, 2, 3]
既に存在するメソッドの再定義?
既に存在するメソッドを再定義した場合は、override になるわけでもコンパイルエラーになるわけでもなく、ただ定義が無視されるようです。
class Printer { def print(str: String) = println(str) } new Printer().print("hoge") new Printer().richPrint("hoge") // error: value richPrint is not a member of Printer class Printable(printer: Printer) { def richPrint(str: String) = println(" *** " + str + " *** ") def print(str: String) = richPrint(str) } implicit def convert(printer: Printer) = new Printable(printer) new Printer().print("hoge") // hoge new Printer().richPrint("hoge") // *** hoge ***
暗黙のパラメータ(implicit parameter)
関数の末尾にある引数リストを implicit 定義すると、既にその型で implicit var/val 定義してある変数が自動的に引数として渡されます。
implicit 定義できるのは最後の引数リストだけです。
関数に対して暗黙の状態をうまく渡したりするときに便利そうです。
case class Someone(name:String) def printMessage(message:String)(implicit s:Someone) = { println(message + " by " + s.name) } printMessage("Hello World") // error: could not find implicit value for parameter s: Someone implicit val someone = Someone("Taro") printMessage("Hello World") // Hello World by Taro
明示的に渡す事で上書きする事もできます。
printMessage("Hello World")(Someone("Jiro")) // Hello World by Jiro
以下のようにひとつだけでなく複数の型を指定することもできます。
def profile(firstName: String) (implicit lastName: String = "", age:Option[Int] = None): String = { (lastName, age) match { case ("", Some(a)) => firstName + "(" + a + ")" case ("", None) => firstName case (l, Some(a)) => firstName + " " + l.toUpperCase + "(" + a + ")" case (l, None) => firstName + " " + l.toUpperCase case _ => firstName } } profile("Taro") // Taro implicit val str: String = "Yamada" implicit val intOrNone: Option[Int] = Some(12) profile("Taro") // Taro YAMADA(12)
やってみよう
暗黙の型変換(implicit conversions)を使って、String型を拡張してみましょう。
"AbcDEf123".reverseOrder() // "321fEDcbA" "AbcDEf123".reverseCases() // "aBCdeF123"
以下は、私が書いてみた例です。
class ReversibleString(val s: String) { def reverseOrder(): String = s.split("").reverse.mkString def reverseCases(): String = s.split("").map { case e if e.toUpperCase == e => e.toLowerCase case e if e.toLowerCase == e => e.toUpperCase case e => e }.mkString } implicit def toReversibleString(s: String) = new ReversibleString(s)
追記:
Daimon.scala で実際にみんなで書いてみました。
勉強会メンバーの中では以下が最終結果になったのですが・・
Scala 練習問題 #daimonscala — Gist
class StringSupport(s: String) { def reverseOrder: String = s.toList.foldLeft(Nil: List[Char])((result, c) => c :: result).mkString def reverseCase: String = s.toList.map { case c if c.isUpper => c.toLower case c if c.isLower => c.toUpper case c => c }.mkString } object StringSupport { implicit def convert(s: String): StringSupport = new StringSupport(s) } import StringSupport._ println("AbcDEf123".reverseOrder) println("AbcDEf123".reverseCase)
@yuroyoro さんが教えてくださったやり方がとても cool でした。
trait Reversible { val s:String def reverseOrder = s reverse def reverseCases = s collect{ case c if c.isUpper => c.toLower case c if c.isLower => c.toUpper case c => c } } implicit def string2Reversible(str:String) = new Reversible{ val s = str } println("AbcDEf123".reverseOrder) println("AbcDEf123".reverseCases)
Daimon.scala は参加者を若干名募集中です(会場の都合でもうあんまり人数入れません・・)。