act-act about projects rss

Implicit Conversion in Scala

Actor model in Scala

先週、ちょっとしたアイデアとアクターモデルの相性が良いことに気がつき、アクターモデルを使ってコードを書いてみたいと思うようになりました。

アクターモデルは、ErlangやScalaのように言語に組み込まれていたり、Akkaのようにライブラリとして提供されています。最近は仕事も仕事以外のコードもJavaで組むことが多いのですが、これを機にErlangかScalaのどちらかを学ぼうと考えました。これまでJavaとJVMに触れている機会が多いことを考え、まずはScalaを学んでみることにしました。

Implicit Conversion

勉強がてら、WEB+DB vol.67の赤黒木のHaskellコードをScalaで書いてみることにしたのですが、何せ命令プログラミング脳なのでなかなか思うように進みません。特に躓くのは、型が合わない際の対処です。Scalaでは暗黙の型変換(implicit conversion)があり、コード上では型が合わなくてもこの機構によりコンパイルが通ることがあります。今回はその暗黙の型変換絡みで理解に時間がかかった点を一つ記載します。

Scalaには、implicitキーワードを付けたメソッドを作成し、その中で型変換を行う処理を記載することで、暗黙の型変換を実現します。

ImplicitConversionTest.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
package net.wrap_trap.scala.examples

object ImplicitConversionTest {
    def main(args : Array[String]) : Unit = {
        val s1 : String = "Foo"
        val i : Int = s1 // compile error 'type mismatch; found : String required: Int' without string2Int 
        System.out.println("i: " + i);
    }
  
    implicit def string2Int(str: String) : Int = {
          1
    }
}

上記のコードでは、StringをIntの1に変換するメソッドstring2Intを定義しています。このメソッドが定義されていない場合、変数iを定義する箇所でコンパイルエラーとなります。実行すると、コンソールには「i: 1」と表示されることから、このメソッドが暗黙の型変換を行っていることが分かります。

Why do Ordered[A] require an implicit conversion from String to StringOps?

今回、すんなり理解できなかったのは以下のコードです。

OrderComparableTest.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package net.wrap_trap.scala.examples

object OrderedComparableTest {
    def main(args : Array[String]) : Unit = {
        val s1 : String = "Hoge"
        val s2 : String = "Bar"
        System.out.println(new Sample1[String](s1).compare(s2))
        System.out.println(new Sample2[String](s1).compare(s2)) // 'Implicit conversions found: s1 => augmentString(s1)'
    }
}

class Sample1[K](val k : Comparable[K]) {
    def compare(k2 : K) : Int = {
        System.out.println("Sample1 type of k: " + k.getClass().getName())
        return k.compareTo(k2)
    }
}

class Sample2[K](val k : Ordered[K]) {
    def compare(k2 : K) : Int = {
        System.out.println("Sample2 type of k: " + k.getClass().getName())
        return k.compareTo(k2)
    }
}

Sample1とSample2ではcompareToメソッドを使って値の比較を行います。Comparable[A]とOrdered[A]は共にtraitで、Ordered[A]はComparable[A]を継承しています。compareToメソッドはComparable[A]でabstract methodとして、Ordered[A]ではconcrete methodとして定義されています。

このコードをScala IDEで見ると、8行目でs1がaugmentStringというメソッドにより型変換されると表示されます。augmentStringというメソッドは暗黙的にインポートされるPredefオブジェクトにて定義されており、StringをStringOpsという別のクラスに変換します。

StringはcompareToメソッドを持っている為、Ordered[A]を使用している22行目、およびComparable[A]を使用している15行目は共にコンパイルエラーにならず、わざわざ型変換する必要は無いように見えます。しかし、何故Ordered[A]を使用しているSample2だけ型変換が行われるのか?という点が理解できませんでした。

Answer

Ordered[A]ではcompareToはconcrete methodなので、メソッドが実装されています。このコードは以下のようになっています。

Ordered.scala

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
trait Ordered[A] extends java.lang.Comparable[A] {

  /** Result of comparing `this` with operand `that`.
   *
   * Implement this method to determine how instances of A will be sorted.
   *
   * Returns `x` where:
   *
   *   - `x < 0` when `this < that`
   *
   *   - `x == 0` when `this == that`
   *
   *   - `x > 0` when  `this > that`
   *
   */
  def compare(that: A): Int

  ...(snip)...

  def compareTo(that: A): Int = compare(that)
}

Ordered[A]のcomareToの実装は、compareというabstract methodの呼び出しとなっていました。こうなると[A]にあたるStringはcompareというメソッドが定義されている必要があるのですが、定義されていません。そしてStringOpsにはcompareというメソッドが定義されており、ここでString->StringOpsへの型変換が行われます。

一方、Comparable[A]のcompareToメソッドはabstract methodなので実装がありません。AはcompareToを実装しているクラスが必要になりますが、StringはcompareToを実装しており、15行目のcompareToではString#compareToが呼び出されます。

Conclusion

Predefで定義されている型変換は他にも色々とあり、型が合わない際に、こちらから呼び出してもいない暗黙の型変換メソッドの名前がコンパイルエラーに含まれてくるので、最初は混乱しました。

Scala IDEは暗黙の型変換がどこで行われるかを表示してくれる為、大分助かります。これをうまく使って、Scalaの型変換に慣れていきたいと思います。