型クラスの雰囲気をつかんでScala標準ライブラリの型クラスを使ってみる回
Scalaで型クラスと言うとScalaz!?となるかもしれませんが, Scala標準ライブラリでも使われています. 提供されている型クラスが意外と知られていない (?) みたいなのでユースケースを交えて簡単にまとめたいと思います.
※ ゆるふわです. 標準ライブラリの範囲なのでモから始まるアイツとかは出てきません.
例えばこんな時… Int
, Long
, Double
等の数値型を統一的に扱いたい… そんな時
まずはオブジェクト指向に則ってこの問題の解決を模索しようと思います. オブジェクト指向と言えば継承ですね. 共通点をインターフェース (トレイト) として括りだして継承 (ミックスイン) してやるのが定石. しかしながら, Int
, Long
, Double
等には継承関係がありません. さて困りました.
早速ですがそこで型クラスの出番です.
本題の前にそもそも型クラスとは
大雑把に一言で表現すると 既存の型に後付けするタイプのインターフェース です. この 既存の型に後付けするタイプ という部分が非常に重要で, 今回考えている問題を解決する鍵になります.
型クラスで数値を表す型を統一的に扱う
では Int
, Long
, Double
が 数値を表す型 に相応しくなるための制約を考えていきます. オブジェクト指向におけるインターフェースの設計と同じです.
- 四則演算可能
- 比較可能
- 定数値の生成
とりあえずこんなところでしょう. 次はコードに落としていきます.
まずは必要なメソッド一覧をまとめます.
// `T` が数値を表す型 trait MyNumeric[T] { def plus(x: T, y: T): T def minus(x: T, y: T): T def times(x: T, y: T): T // 省略 }
次に各型に対してそれぞれの実装を書いていきます.
val numericInt = new MyNumeric[Int] { def plus(x: Int, y: Int): Int = x + y def minus(x: Int, y: Int): Int = x - y def times(x: Int, y: Int): Int = x * y // 省略 } val numericLong = new MyNumeric[Long] { def plus(x: Long, y: Long): Long = x + y def minus(x: Long, y: Long): Long = x - y def times(x: Long, y: Long): Long = x * y // 省略 } val numericDouble = new MyNumeric[Double] { def plus(x: Double, y: Double): Double = x + y def minus(x: Double, y: Double): Double = x - y def times(x: Double, y: Double): Double = x * y // 省略 }
自由に定義できるので, 例えば String
に対しても実装できます.
val numericString = new MyNumeric[String] { def plus(x: String, y: String): String = x + y def minus(x: String, y: String): String = x diff y def times(x: String, y: String): String = x + y + "times" // 省略 }
さて, これで定義した型を数値として扱うための規則を後付けで実装できました. つまり以下のように計算が可能になりました.
val x: Int = 10 val y: Int = 20 println(numericInt.plus(x, y)) // => 30 val s: String = "abc" val t: String = "a" println(numericString.minus(s, t)) // => bc
この規則を列挙した MyNumeric
に当たるものを型クラスと言います. 型クラスというと仰々しく聞こえるかもしれませんが実物はこの通り, ただのトレイトです (Scala-domesticの話, 例えばHaskellでは言語レベルでサポートされている). 今回は数値を表すという条件だったので MyNumeric
としていますが, 条件によって様々な型クラスが考えられます.
いい感じに操作ができるようになりましたが2つほど気になる点がでてきます.
numericXXX
(型クラスのインスタンス) の扱いが大変じゃ…- いちいち全部定義しなきゃいけないの?
一つ目の型クラスのインスタンスの問題から解決していきましょう. これには implicit
を使います.
ある数値 x
を自乗するための関数 square
を例に考えます.
def square[T](x: T): T
このままでは T
に制約がないため自乗を定義できません. そこで先程の MyNumeric
を制約として与え, その times
メソッドを利用することにしましょう.
// `implicit` をお忘れなく implicit val numericInt = new MyNumeric[Int] { def plus(x: Int, y: Int): Int = x + y def minus(x: Int, y: Int): Int = x - y def times(x: Int, y: Int): Int = x * y // 省略 } def square[T](x: T)(implicit num: MyNumeric[T]): T = num.times(x, x) val i: Int = 10 println(square(i)) // => 100
ここでのImplicitパラメータは MyNumeric
を実装した T
型のインスタンスがあればコンパイルできることを意味します. 裏を返すと, MyNumeric
を実装していない T
型の変数を square
の引数に渡してもコンパイルエラーになります. これで numericXXX
をImplicitパラメータとして渡せる状態にさえしておけば, 毎度毎度トレイト実装とインスタンス作成する必要はないことが分かりました. implicit
が一番輝いている瞬間ですね (元々 implicit
はScalaで型クラスを表現するために生まれた機能だったはず).
次に二つ目の定義の問題です. MyNumeric
と特定の型 (Int
, Long
, Double
等) に限って言えば, 相当する型クラスとその実装が Numeric
型クラスとその実装として 標準ライブラリで提供されています. 使い方は相変わらずです.
// Numeric[T] に変わった def square[T](x: T)(implicit num: Numeric[T]): T = num.times(x, x)
ここでScala標準ライブラリで提供されている型クラスの一部を紹介します.
Numeric
数値として操作可能であることを表します. 使い方は紹介してきた通り.
Equiv
等価比較が可能であることを表します. 他言語だと Eq
とされる場合が多い (多分).
Ordering
ソートが可能であることを表します. 他言語だと Ord
とされる場合が多い (多分).
Fractional
Numeric
に加えて割り算 (div) 可能であることを表します.
Integral
Numeric
に加えて割り算 (quot) 可能であることを表します.
ちなみにオブジェクト指向風に class Piyo extends Numeric[Piyo] { ... }
とすることもできますが, 当然既存クラスに対しては実装できません.
おまけ
便利なImplicit
importしてやるとお馴染みの記号で演算できるようになります.
こんな感じ.
def square[T](x: T)(implicit num: Numeric[T]): T = num.times(x, x) ↓ def square[T](x: T)(implicit num: Numeric[T]): T = { import num._ // or import Numeric.Implicits._ x * x }
def gt[T](x: T, y: T)(implicit ord: Ordering[T]): Boolean = ord.gt(x, y) ↓ def gt[T](x: T, y: T)(implicit ord: Ordering[T]): Boolean = { import ord._ // or import Ordering.Implicits._ x > y }
これらだけでなく, 標準ライブラリで提供されてる型クラスには大体あります.
Context bound記法
ただの糖衣構文. 型クラスのインスタンスの取得しやすさが違ったりするのでお好みで使い分けて.
def square[T](x: T)(implicit num: Numeric[T]): T = num.times(x, x) // ↕ 等価. ただし型クラスのインスタンスは `implicitly` で取得する必要がある def square[T : Numeric](x: T): T = { val num = implicitly[Numeric[T]] num.times(x, x) }