2つの型クラスを使う

任意のリストの合計を計算する sum メソッドを作ったときには、「足すことのできる」型クラスとして Additive という型クラスと、そのインスタンス IntAdditive、StringAdditive を作った。

今回は、リストの平均を求める average メソッドを作ってみよう。Int に限れば次のように定義することができる。要素を合計して要素数で割っているだけだ。

scala> def average(lst: List[Int]): Int = lst.foldLeft(0)((x, y) => x + y) / lst.length
average: (lst: List[Int])Int

使い方も簡単。

scala> average(List(1, 3, 5))
res0: Int = 3

さて、この average メソッドを、Int でも Double でも使えるようにしたい。

合計を求めるのには Additive を使えばいいだろう。だけどそれだけじゃ足りない。1つには、要素数で割る必要があるので割り算もできなければならない。もう1つは、要素数を求める length メソッドは Int を返すので、これを合計の型(Int か Double)に合わせてやる必要がある。

そこで、Additive をもっと一般化して、四則演算とゼロを持つ Num という型クラスを考えよう。で、Int と Double にそれぞれ対応する IntNum と DoubleNum というインスタンスを作る。ファイルは Num.scala。

trait Num[A] {
    def plus(a: A, b: A): A
    def minus(a: A, b: A): A
    def multiply(a: A, b: A): A
    def divide(a: A, b: A): A
    def zero: A
}

object Num {
    implicit object IntNum extends Num[Int] {
        def plus(a: Int, b: Int): Int = a + b
        def minus(a: Int, b: Int): Int = a - b
        def multiply(a: Int, b: Int): Int = a * b
        def divide(a: Int, b: Int): Int = a / b
        def zero: Int = 0
    }
    implicit object DoubleNum extends Num[Double] {
        def plus(a: Double, b: Double): Double = a + b
        def minus(a: Double, b: Double): Double = a - b
        def multiply(a: Double, b: Double): Double = a * b
        def divide(a: Double, b: Double): Double = a / b
        def zero: Double = 0.0
    }
}

それから、Int から変換する FromInt という型クラスと、FromIntToInt、FromIntToDoubleというインスタンスを作る。ファイルは FromInt.scala。

trait FromInt[A] {
    def to(from: Int): A
}

object FromInt {
    implicit object FromIntToInt extends FromInt[Int] {
        def to(from: Int): Int = from
    }
    implicit object FromIntToDouble extends FromInt[Double] {
        def to(from: Int): Double = from
    }
}

sbt console を起動して2つのファイルを読み込めば、準備は完了。

average メソッドの定義は次のようになる。

scala> def average[A](lst: List[A])(implicit a: Num[A], b: FromInt[A]): A = {
     |     val length: Int = lst.length
     |     val sum: A = lst.foldLeft(a.zero)((x, y) => a.plus(x, y))
     |     a.divide(sum, b.to(length))
     | }
average: [A](lst: List[A])(implicit a: Num[A], implicit b: FromInt[A])A

ここで1つ勘違いをしていたのを白状しよう。「implicit キーワードは引数リストの先頭にしか付けられない」というので、てっきり先頭の引数だけが implicit になるのだと思っていた。実際には引数リスト全体が implicit になるんだね。というわけで、ここでは a と b が implicit parameter になっている。言い換えると Num と FromInt という2つの型クラスを使っている。

さあ、最後に試してみよう。

scala> average(List(1, 3, 5))
res0: Int = 3

scala> average(List(1.5, 2.5, 3.5))
res1: Double = 2.5

このとおり、Int にも Double にも対応した average メソッドができた。

Implicitの探索範囲

Implicit Conversion や Implicit Parameter が探索される範囲には、次のような範囲がある。

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

ここでは、コンパニオンオブジェクトに Implicit を定義するパターンを見てみる。

新しく Rational (有理数)型を定義するとして、次のように定義する。ここでは Rational.scala ファイルに書いた。

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

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

object Rational {
    implicit object RationaAdditive 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)
    }
}

同じディレクトリで sbt console を起動すると自動的に読み込まれる。

で、sum メソッドを定義。なんでファイルに書かないかというと、クラスやトレイトに属さないメソッドはファイルには書けない(たぶん)から。

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(Rational(1, 2), Rational(1, 3)))
res0: Rational = Rational(5,6)

ちゃんと計算できた。

コンパニオンオブジェクトに Implicit を定義しておくのはわかりやすいかもしれないな。覚えておこう。

型クラス

前回のエントリで見たような Implicit Parameter の使い方を、「型クラス(を使った計算)」という。

これは Haskell から持ってきたもので、Haskell の用語では Additive を型クラス、StringAdditive や IntAdditive をそのインスタンスと呼ぶ。

と、ここまで研修資料の通りに書いたところで違和感を覚えた。型クラスとインスタンスは Haskell をやったことがあるので覚えている。Implicit Parameter の使い方というよりは、型パラメータを持った Additive と具体的な型について実装した StringAdditive や IntAdditive の使いかたを型クラスとインスタンスとよび、Implicit Parameter を使うと簡潔に書ける、といったほうが正解だろう(と思う)。

いずれにしても Scala において型クラスとインスタンスは重要な位置を占めているらしいので、次回からもう少し見ていく。

Implicit Parameter (2)

Implicit Parameter のもう1つの使い方。順を追ってみていく。

何らかのリストの合計を求めるメソッド sum を考える。ポイントは何のリストかわからない(あるいは何のリストでもいいように)、というところだ。

Scala は静的型付け言語なので単純にはいかない。ではどうするかというと、まず、「足し合わせることができる」型を考える。Additive としよう。

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

Additive は型パラメータを持っていて、これが目的のリストに要素の型になる。また、実装はしていないがメソッドを2つ宣言している。

  • plus :2つの値を足し合わせる
  • zero :ゼロに相当する値を返す

だ。

次に、この型を継承して、具体的な型についてメソッドを実装する。String と Int について実装しよう。

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

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

そして最後に sum メソッド。Additive を使って、足し合わせるように実装する。

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

これで出来上がり。2つ目の引数リストで Additive を受け取るところがポイント。使い方はこうする。

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

scala> sum(List("abc", "def", "ghi"))(StringAdditive)
res1: String = abcdefghi

うまく「合計」を計算できた。

さて、ここからが本題。何のリストを合計するのかはリストを見ればわかるのだから、StringAdditive とか IntAdditive を明示的に渡さなくてもうまくやってほしい。Implicit Parameter を使うとそれができる。

次のように、StringAdditive を IntAdditive の定義の前と、sum メソッドの最後に引数リストの m の前に implicit キーワードをつける。

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

これで StringAdditive や IntAdditive を明示的に渡さなくてもよくなる。

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

scala> sum(List("abc", "def", "ghi"))
res1: String = abcdefghi

このとおり。

Implicit Parameter

Implicit Parameter は暗黙の引数だ。2つの使い方があるようだけど、今日はその1つ、あちこちで共通に使われる引数を、いちいち明示的に渡すのを省略するための使い方を見ていこう。

参考にしている Dwango の研修資料では、データベースのコネクションの例が示されているけど、ここではもっと簡単な例を示す。

scala> def add(x: Int)(implicit y: Int): Int = x + y
add: (x: Int)(implicit y: Int)Int

scala> def sub(x: Int)(implicit y: Int): Int = x - y
sub: (x: Int)(implicit y: Int)Int

scala> def mul(x: Int)(implicit y: Int): Int = x * y
mul: (x: Int)(implicit y: Int)Int

Implicit Parameter を使うには、引数宣言に implicit キーワードをつけるだけだ。ただし、implicit キーワードをつけられるのは引数リストの最初に引数だけ、という制約があるので、通常は複数の引数リストを持つメソッドにして最後に Implicit Parameter を持ってくるようだ。

さて、これらのメソッドを呼び出すと、Scala は現在のスコープで直近にある implicit とマークされた変数の値を、Implicit Parameter としてメソッドに引き渡す。具体的には次のようにする。

scala> implicit val z: Int = 2
z: Int = 2

scala> add(3)
res0: Int = 5

scala> sub(3)
res1: Int = 1

scala> mul(3)
res2: Int = 6

implicit val で宣言された変数 z が、メソッド呼び出しの時に暗黙に引き渡されている。

ここで、新しい変数を implicit で宣言したらどうなるだろう。

scala> implicit val z1: Int = 5
z1: Int = 5

scala> add(3)
<console>:15: error: ambiguous implicit values:
 both value z of type => Int
 and value z1 of type => Int
 match expected type Int
       add(3)
          ^

おや、エラーになった。どうやら、期待する Implicit Parameter にマッチする値が2つあるせいのようだ。こういう使い方はできないらしい。

明示的に渡してやったらどうだろう。

scala> add(3)(z1)
res4: Int = 8

ああ、これは普通にいけるのね。

というかさあ、こういうあちこちで使いまわす変数っていうのは、オブジェクト指向的にはオブジェクトのメンバー変数にしておくんだろうけど、Scala では違うんだろうか。

Implicit Conversion

Implicit Conversion とは、暗黙の型変換をユーザが定義できるようにする機能、だそうだ。例えば、真偽値が必要なところにユーザ定義型が来たとき、その型を真偽値に変換する Implicit Conversion が定義されていれば、エラーにならずに変換される、ってことらしい。

Implicit Conversion の定義は implicit キーワードをつけたメソッド定義だと思えばいい。あとは引数が1つであることくらいか。Int を Boolean に変換する Implicit Conversion の定義はこんな感じ。

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

メソッド名はたぶん何でもいい。重要なのは引数の型と返り値の型。次の例のように、Boolean でなければいけないところ(if の条件式)に Int が来たとき、Scala は Implicit Conversion を探して、あてはまるものが見つかったらそれを適用してくれる。その結果、次のようになる。

scala> if (1) {
     |     println("1 is true.")
     | }
1 is true.

本来、if の条件式は Boolean でなければならないけど、暗黙の型変換が働いて Int を Boolean に変換した結果、1 is true. と出力されている。

もっとも、この使い方はあまり好ましくないようだ。

もうひとつの使い方は、pimp my library パターンとよばれ、既存のクラスにメソッドを追加して拡張するように見せかける使い方だ。例として、文字列の最後にスマイルマーク :-) をくっつけて返す 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

enrichString が Implicit Conversion だ。String を RichString に変換する。次のように使う。

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

“Hi, ” は String なので smile なんていうメソッドは持っていない。だけど Implicit Conversion のおかげで暗黙に RichString に変換されて、smile メソッドが呼ばれている。

さて、上の例は Scala 2.10 以降では Implicit Class という機能で実現できる。

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 専用の機能なので、通常はこっちを使うといいようだ。

Try

Try には Either と同じように2つの値がある。Success と Failure だ。Either と違うのは、Failure には Throwable の値しか入れられないことだ。こんな感じ。

scala> import scala.util.Try
import scala.util.Try

scala> val v: Try[Int] = Try(throw new RuntimeException("to be caught"))
v: scala.util.Try[Int] = Failure(java.lang.RuntimeException: to be caught)

Throwable ってなに?ってかんじだけど、Java の例外みたいなものかな。

Failure には Throwable しか入れられないので、Try は1つだけ型パラメータをとって、それが Success の中身の型になる。

scala> val v1 = Try(3)
v1: scala.util.Try[Int] = Success(3)

うーん、まだ消化不良だけど、今日は時間がないのでここまで。

Either

Option には2つの値 Some と None がある。Some はある種のコンテナで中に別の値を持つことができるけど、None は値を持つことができない。

つまり、エラーが起こったこと自体は None で知らせることができても、どんなエラーなのかはわからないわけだ。そこで Either の登場だ。

Either にも2つの値 Right と Left があって、両方とも中身の値を持つことができる。通常は、正常だった時に Right を、何かエラーがあったときに Left を使う。Either は2つの型パラメータをとる。つまり Either[A, B] で、A は Left の中身の型、B は Right の中身の型だ。

scala> val v1: Either[String, Int] = Right(123)
v1: Either[String,Int] = Right(123)

scala> val v2: Either[String, Int] = Left("abc")
v2: Either[String,Int] = Left(abc)

もちろん、パターンマッチもできる。

scala> v1 match {
     |     case Right(i) => println(i)
     |     case Left(s)  => println(s)
     | }
123

Option

今日から Scala のエラーを表すデータ型を見ていく。

最初は Option 型だ。Option は言ってみれば値を1つだけ入れられるコンテナで、値の入っている Some と何もないっていないことを表す None の2つの値がある。

っていうか、これ、OCmal の Option と同じだよね。Haskell で言えば Maybe だ。

Option は、たとえば次のように作れて、動作する。

scala> val o1: Option[String] = Option("hoge")
o1: Option[String] = Some(hoge)

scala> o1.isEmpty
res0: Boolean = false

scala> o1.isDefined
res1: Boolean = true

scala> o1.get
res2: String = hoge

Option はコンテナなので型パラメータを持つ。上の例では String だ。で、持っている値は get メソッドで取得できる。

じゃあ、null を入れた場合はどうだろう。

scala> val o2: Option[String] = Option(null)
 o2: Option[String] = None

scala> o2.isEmpty
res3: Boolean = true

scala> o2.isDefined
res4: Boolean = false

scala> o2.get
java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:349)
  at scala.None$.get(Option.scala:347)
  … 36 elided

null を入れると Option の値としては None になる。っていうか null ってなに?Java の null?

まあいいか。とにかく中身の値がないので、get メソッドではエラーになっている。

None も Option の値なので、isEmpty とか isDefined とかのメソッドに対してはちゃんと値を返している。もうひとつ、Option には便利な getOrElse メソッドがあって None だった場合には別の値を返すことができるようになっている。

scala> o2.getOrElse("no value")
res6: String = no value

最後に、パターンマッチを見よう。

scala> val s: Option[String] = Option("hoge")
s: Option[String] = Some(hoge)

scala> val result = s match {
     |     case Some(str) => str
     |     case None => "not matched"
     | }
result: String = hoge

Option は例外と違ってふつうのデータ型なので、パターンマッチができる。例外処理を書くんではなくて、ほかのデータ型と同じような処理をかけるわけだ。

部分関数

よくわからないものが出てきた。説明の前に使い方を見てみよう。

scala> List(1,2,3,4,5).collect { case i if i % 2 == 1 => i * 2 }
res0: List[Int] = List(2, 6, 10)

リストの collect メソッドは match 式の { } で囲まれたブロックの部分だけを引数にとって、条件に合う要素だけを抜き出し、=> の右の式を適用した新しいリストを返す(でいいのかな)。この引数が部分関数(PartialFunction)らしい。

引数、とさらっと言ったけど、引数はカッコに囲まれていない。あっちゃダメなのかと思ったらそうでもないようだ。

scala> List(1,2,3,4,5).collect({ case i if i % 2 == 1 => i * 2 })
res1: List[Int] = List(2, 6, 10)

collect メソッドの動作はわかるんだけど、その引数が部分関数だといわれてもちょっとピンとこない。部分関数ってどうして部分?

もうちょっと厳密に言うと、引数に書いてあるブロック式自体は部分関数ではなくて、この式から部分関数が生成されるようだ。一般には case が複数あって、どれかのパターンにマッチしたときだけ新しい値がつくられてリストの要素になる。

てことは、こんなふうにも書けるわけだ。

scala> List(1,2,3,4,5).collect {
     |     case i if i % 2 == 0 => i * 2
     |     case i if i % 2 == 1 => i * i
     | }
res2: List[Int] = List(1, 4, 9, 8, 25)

うん、期待通りの動作はしてる。

でも、やっぱりなんで部分関数っていうんだかわからないなぁ。