seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Scalaのimplicit(暗黙)入門

Scalaのバージョン

この記事が対象とするScalaのバージョンは「2.9.1.final」です。

暗黙の型変換(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 で実際にみんなで書いてみました。

Daimon.scala #7 - Togetter

勉強会メンバーの中では以下が最終結果になったのですが・・

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 は参加者を若干名募集中です(会場の都合でもうあんまり人数入れません・・)。

Daimon.scala