implicit conversion(暗黙の型変換)とimplicit parameter(暗黙のパラメータ)

Scalaには、他の言語にはあまり見られない、implicit conversionとimplicit parameterという機能があります。この2つの機能を上手く使いこなすことができれば、Scalaでのプログラミングの生産性は劇的に向上するでしょう。なお、本当は、implicit conversionとimplicit parameterは相互に関係がある2つの機能なのですが、今回学習する範囲では意識する必要がないと思われるので、2つの独立した機能として学びます。

Implicit Conversion

implicit conversionは暗黙の型変換機能をユーザが定義できるようにする機能です。

implicit conversionは

  implicit def メソッド名(引数名: 引数の型): 返り値の型 = 本体

という形で定義します。implicitというキーワードがついていることと引数が1つしかない1ことを除けば通常のメソッド定義と同様ですね。さて、implicit conversionでは、引数の型と返り値の型に重要な意味があります。それは、これが、引数の型の式が現れたときに返り値の型を暗黙の変換候補として登録することになるからです。

定義したimplicit conversionは大きく分けて二通りの使われ方をします。1つは、新しく定義したユーザ定義の型などを既存の型に当てはめたい場合です。たとえば、

scala> implicit def intToBoolean(arg: Int): Boolean = arg != 0
warning: there was one feature warning; for details, enable `:setting -feature' or `:replay -feature'
intToBoolean: (arg: Int)Boolean

scala> if(1) {
     |   println("1は真なり")
     | }
1は真なり

といった形で、本来Booleanしか渡せないはずのif式にIntを渡すことができています。ただし、この使い方はあまり良いものではありません。上の例をみればわかる通り、implicit conversionを定義することで、コンパイラにより、本来はif式の条件式にはBoolean型の式しか渡せないようにチェックしているのを通りぬけることができてしまうからです。一部のライブラリではそのライブラリ内のデータ型とScala標準のデータ型を相互に変換するために、そのようなimplicit conversionを定義している例がありますが、本当にそのような変換が必要かよく考える必要があるでしょう。

pimp my library

もう1つの使い方は、pimp my libraryパターンと呼ばれ、既存のクラスにメソッドを追加して拡張する(ようにみせかける)使い方です。 Scala標準ライブラリの中にも大量の使用例があり、こちらが本来の使い方と言って良いでしょう。たとえば、これまでみたプログラムの中に(1 to 5)という式がありましたが、本来Int型はtoというメソッドを持っていません。 toメソッドはpimp my libraryパターンの使用例の最たるものです。コンパイラは、ある型に対するメソッド呼び出しを見つけたとき、そのメソッドを定義した型がimplicit conversionの返り値の型にないか探索し、型が合ったらimplicit conversion の呼び出しを挿入するのです。この使い方の場合、implicit conversionの返り値の型が他で使われるものでなければ安全に implicit conversionを利用することができます。

試しに、Stringの末尾に":-)"という文字列を追加して返すimplicit conversionを定義してみましょう。

scala> class RichString(val src: String) {
     |   def smile: String = src + ":-)"
     | }
defined class RichString

scala> implicit def enrichString(arg: String): RichString = new RichString(arg)
warning: there was one feature warning; for details, enable `:setting -feature' or `:replay -feature'
enrichString: (arg: String)RichString

scala> "Hi, ".smile
res1: String = Hi, :-)

ちゃんと文字列の末尾に":-)"を追加するsmileメソッドが定義できています。さて、ここでひょっとしたら気がついた方もいるかもしれませんが、implicit conversionはそのままでは、既存のクラスへのメソッド追加のために使用するには冗長であるということです。Scala 2.10からは、classにimplicit というキーワードをつけることで同じようなことができるようになりました(皆さんが学習するのはScala 2.12なので気にする必要はありません。

Implicit Class

上の定義は、Scala 2.10以降では、

scala> implicit class RichString(val src: String) {
     |   def smile: String = src + ":-)"
     | }
defined class RichString

scala> "Hi, ".smile
res0: String = Hi, :-)

という形で書きなおすことができます。 implicit classはpimp my libraryパターン専用の機能であり、implicit defで既存型への変換した場合などによる混乱がないため、Scala 2.10以降でpimp my libraryパターンを使うときは基本的に後者の形式にすべきですが、サードパーティのライブラリや標準ライブラリでも前者の形式になっていることがあるので、そのようなコードも読めるようにしておきましょう。

練習問題

IntからBooleanへのimplicit conversionのように利用者を混乱させるようなものを考えて、定義してみてください。また、そのimplicit conversionにはどのような危険があるかを考えてください。

練習問題

pimp my libraryパターンで、既存のクラスの利用を便利にするようなimplicit conversionを1つ定義してみてください。それはどのような場面で役に立つでしょうか?

練習問題

Scala標準ライブラリの中からpimp my libraryが使われている例を(先ほど挙げたものを除いて)1つ以上見つけてください。

Implicit Parameter

implicit parameterは主として2つの目的で使われます。1つの目的は、あちこちのメソッドに共通で引き渡されるオブジェクト(たとえば、ソケットやデータベースのコネクションなど)を明示的に引き渡すのを省略するために使うものです。これは例で説明すると非常に簡単にわかると思います。

まず、データベースとのコネクションを表すConnection型があるとします。データベースと接続するメソッドは全てこのConnection型を引き渡さなければなりません。

def useDatabase1(...., conn: Connection)
def useDatabase2(...., conn: Connection)
def useDatabase3(...., conn: Connection)

この3つのメソッドは共通してConnection型を引数に取るのに、呼びだす度に明示的にConnectionオブジェクトを渡さなければならず面倒で仕方ありません。ここでimplicit parameterの出番です。上のメソッド定義を

def useDatabase1(....)(implicit conn: Connection)
def useDatabase2(....)(implicit conn: Connection)
def useDatabase3(....)(implicit conn: Connection)

のように書き換えます。implicit修飾子は引数の先頭の要素に付けなければならないという制約があり、implicit parameterを使うにはカリー化されたメソッド定義が必要になります。最後の引数リストが

(implicit conn: Connection)

とあるのがポイントです。Scalaコンパイラは、このようにして定義されたメソッドが呼び出されると、現在のスコープからたどって直近のimplicitとマークされた値を暗黙にメソッドに引き渡します。値をimplicitとしてマークするとは、たとえば次のようにして行います:

implicit val connection: Connection = connectDatabase(....)

このようにすることで、最後の引数リストに暗黙にConnectionオブジェクトを渡してくれるのです。このようなimplicit parameterの使い方はPlay 2 FrameworkやScalaの各種O/Rマッパーで頻出します。

implicit parameterのもう1つの使い方は、少々変わっています。まず、Listの全ての要素の値を加算した結果を返すsumメソッドを定義したいとします。このメソッドはどのような定義になるでしょうか。ポイントは、「何の」Listか全くわかっていないことで、整数の+メソッドをそのまま使ったりということはそのままではできないということです。このような場合、2つの手順を踏みます。

まず、2つの同じ型を足す(0の場合はそれに相当する値を返す)方法を知っている型を定義します。ここではその型をAdditiveとします。 Additiveの定義は次のようになります:

trait Additive[A] {
  def plus(a: A, b: A): A
  def zero: A
}

ここで、Additiveの型パラメータAは加算されるListの要素の型を表しています。また、

  • zero: 型パラメータAの0に相当する値を返す
  • plus: 型パラメータAを持つ2つの値を加算して返す

です。

次に、このAdditive型を使って、Listの全ての要素を合計するメソッドを定義します:

def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))

後は、それぞれの型に応じた加算と0の定義を持ったobjectを定義します。ここではStringIntについて定義をします。

object StringAdditive extends Additive[String] {
  def plus(a: String, b: String): String = a + b
  def zero: String = ""
}

object IntAdditive extends Additive[Int] {
  def plus(a: Int, b: Int): Int = a + b
  def zero: Int = 0
}

まとめると次のようになります。

trait Additive[A] {
  def plus(a: A, b: A): A
  def zero: A
}

object StringAdditive extends Additive[String] {
  def plus(a: String, b: String): String = a + b
  def zero: String = ""
}

object IntAdditive extends Additive[Int] {
  def plus(a: Int, b: Int): Int = a + b
  def zero: Int = 0
}

def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))

これで、Int型のListString型のListのどちらの要素の合計も計算できる汎用的なsumメソッドができました。実際に呼び出したいときには、

scala> sum(List(1, 2, 3))(IntAdditive)
res2: Int = 6

scala> sum(List("A", "B", "C"))(StringAdditive)
res3: String = ABC

とすれば良いだけです。さて、これで目的は果たすことはできましたが、何のListの要素を合計するかは型チェックする時点ではわかっているのだからいちいちIntAdditive, StringAdditiveを明示的に渡さずとも賢く推論してほしいものです。そして、まさにそれをimplicit parameterで実現することができます。方法は簡単で、StringAdditiveIntAdditiveの定義の前にimplicitと付けることと、sumの最後の引数リストのmにimplicitを付けるだけです。implicit parameterを使った最終形は次のようになります。

scala> trait Additive[A] {
     |   def plus(a: A, b: A): A
     |   def zero: A
     | }
defined trait Additive

scala> implicit object StringAdditive extends Additive[String] {
     |   def plus(a: String, b: String): String = a + b
     |   def zero: String = ""
     | }
defined object StringAdditive

scala> implicit object IntAdditive extends Additive[Int] {
     |   def plus(a: Int, b: Int): Int = a + b
     |   def zero: Int = 0
     | }
defined object IntAdditive

scala> def sum[A](lst: List[A])(implicit m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
sum: [A](lst: List[A])(implicit m: Additive[A])A

scala> sum(List(1, 2, 3))
res4: Int = 6

scala> sum(List("A", "B", "C"))
res5: String = ABC

任意のListの要素の合計値を求めるsumメソッドを自然な形(sum(List(1, 2, 3)))で呼びだすことができています。実は、implicit parameterのこのような使い方はプログラミング言語Haskellから借りてきたもので、型クラス(を使った計算)と言われます。Haskellの用語だと、Additiveを型クラス、StringAdditiveIntAdditiveAdditive型クラスのインスタンスと呼びます。

このimplicit parameterの用法は標準ライブラリにもあって、たとえば、

scala> List[Int]().sum
res6: Int = 0

scala> List(1, 2, 3, 4).sum
res7: Int = 10

scala> List(1.1, 1.2, 1.3, 1.4).sum
res8: Double = 5.0

のように整数や浮動小数点数の合計値を特に気にとめることなく計算することができています。Scalaにおいて型クラスを定義・使用する方法を覚えると、設計の幅がグンと広がります。

練習問題

m: Additive[T]と値t1: T, t2: T, t3: Tは、次の条件を満たす必要があります。

m.plus(m.zero, t1) == t1  // 単位元
m.plus(t1, m.zero) == t1  // 単位元
m.plus(t1, m.plus(t2, t3)) == m.plus(m.plus(t1, t2), t3) // 結合則

このような条件を満たす型Tと単位元zero、演算plusを探し出し、Additive[T]を定義しましょう。この際、条件が満たされていることをいくつかの入力に対して確認してみましょう。また、定義したAdditive[T]implicitにして、Tの合計値を先ほどのsumで計算できることを確かめてみましょう。

ヒント:このような条件を満たすものは無数にありますが、思いつかない人はたとえばx座標とy座標からなる点を表すクラスPointを考えてみると良いでしょう。

練習問題

List[Int]List[Double] のsumを行うために、標準ライブラリでは何という型クラス(1つ)と型クラスのインスタンス(2つ)を定義しているかを、Scala標準ライブラリから探して挙げなさい。

implicitの探索範囲

implicit defやimplicit parameterの値が探索される範囲には、

  • ローカルで定義されたもの
  • importで指定されたもの
  • スーパークラスで定義されたもの
  • コンパニオンオブジェクトで定義されたもの

などがあります。この中で注目していただきたいのが、コンパニオンオブジェクトでimplicitの値を定義するパターンです。

たとえば新しくRational(有理数)型を定義したとして、コンパニオンオブジェクトに先ほど使ったAdditive型クラスのインスタンスを定義しておきます。

case class Rational(num: Int, den: Int)

object Rational {
  implicit object RationalAdditive extends Additive[Rational] {
    def plus(a: Rational, b: Rational): Rational = {
      if (a == zero) {
        b
      } else if (b == zero) {
        a
      } else {
        Rational(a.num * b.den + b.num * a.den, a.den * b.den)
      }
    }
    def zero: Rational = Rational(0, 0)
  }
}

すると、importをしていないのに、このAdditive型クラスのインスタンスを使うことができます。

scala> sum(List(Rational(1, 1), Rational(2, 2)))
res0: Rational = Rational(4,2)

新しくデータ型を定義し、型クラスインスタンスも一緒に定義したい場合によく出てくるパターンなので覚えておくとよいでしょう。

1. 引数が2つ以上あるimplicit defの定義も可能です。「implicit defのパラメーターにimplicitが含まれる」という型クラス的な使い方をする場合は実際にimplicit defに2つ以上のパラメーターが出現することがあります。ただしそういった定義は通常implicit conversionとは呼ばれません

results matching ""

    No results matching ""