水底

ScalaとかC#とかk8sとか

型クラスの雰囲気をつかんで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 が一番輝いている瞬間ですね (元々 implicitScalaで型クラスを表現するために生まれた機能だったはず).

次に二つ目の定義の問題です. 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)
}