Type-Classパターンのおぼえがき

Qiitaからの転記です。

Type-classパターンの覚え書きです。 使わないと忘れてしまう自分用メモです。

Type-classを使う前の実装

これだと以下のパターンマッチを使ったコードがあったとして、

case class User(name: String, age: Int, email: String)

object HTMLSerializerPatternMatch {
  def serialize(value: Any): String = value match {
    case User(name, age, email) => s"<div>${name} (${age} yo) <a href=${email}/> </div>"
    case i: Int =>  s"<div style: color=blue>$value</div>"
}

良くない点が以下

  • 型安全でない。(Anyを使っている)
  • マッチングしたい型が増えたときに、毎回コードの修正が発生する。

上記のコードをType-classを使って改善する。

Type-classを使った実装

// 型クラス
trait HTMLSerializer[T] {
  def serialize(value: T): String
}

// 型クラスのコンパニオンオブジェクト
object HTMLSerializer {
  def serialize[T](value: T)(implicit serializer: HTMLSerializer[T]): String =
    serializer.serialize(value)

  def apply[T](implicit serializer: HTMLSerializer[T]) = serializer
}

// 型クラスのインスタンスがInt
implicit object IntSerializer extends HTMLSerializer[Int] {
  override def serialize(value: Int): String = 
    s"<div style: color=blue>$value</div>"
}
// 型クラスのインスタンスがUser
implicit object UserSerializer extends HTMLSerializer[User] {
  override def serialize(user: User): String = 
    s"<div>${user.name} (${user.age} yo) <a href=${user.email}/> </div>"
}

implicit serializer: HTMLSerializer[T]serializerIntSerializerもしくはUserSerializerが暗黙的に渡される。Stragegyパターンに似たことをimplicitで表現できる。

val value1 = User("48hands", 48, "hogehoge@example.com")
val value2 = 48

println(HTMLSerializer.serialize(value1))
println(HTMLSerializer.serialize(value2))

Type-classとimplicit conversion class組み合わせ

以下のようなimplicitなクラスを定義する。 TにはUserIntが入ることになる。

implicit class HTMLEnricher[T](value: T) {
  def toHTML(implicit serializer: HTMLSerializer[T]): String = serializer.serialize(value)
}

以下のように利用できる。

val user = User("48hands", 22, "48hands@example.com")
val number = 48

println(user.toHTML)
println(number.toHTML)

実行結果。

<div>48hands (22 yo) <a href=48hands@example.com/> </div>
<div style: color=blue>48</div>

コード全部のせておきます。

case class User(name: String, age: Int, email: String)

package object serializer {
  // 型クラス
  trait HTMLSerializer[T] {
    def serialize(value: T): String
  }

  // 型クラスのコンパニオンオブジェクト
  object HTMLSerializer {
    def serialize[T](value: T)(implicit serializer: HTMLSerializer[T]): String =
    serializer.serialize(value)

    def apply[T](implicit serializer: HTMLSerializer[T]) = serializer
  }

  // 型クラスのインスタンス
  implicit object UserSerializer extends HTMLSerializer[User] {
    override def serialize(user: User): String = s"<div>${user.name} (${user.age} yo) <a href=${user.email}/> </div>"
  }

  // 型クラスのインスタンス
  implicit object IntSerializer extends HTMLSerializer[Int] {
    override def serialize(value: Int): String = s"<div style: color=blue>$value</div>"
  }
}

package object richer {
  implicit class HTMLEnricher[T](value: T) {
    import serializer._
    def toHTML(implicit serializer: HTMLSerializer[T]): String = serializer.serialize(value)
  }
}

// エンドポイント
object TypeClassSandbox extends App {
  import richer._

  val user = User("48hands", 22, "48hands@example.com")
  val number = 48

  println(user.toHTML)
  println(number.toHTML)
}

(object HTMLSerializerはなくても動きます。)

まとめると、基本構成はざっくり以下になっている、

// Type class
trait MyTypeClassTemplate[T] {
  def doSomething(value: T): String
}
// Type class instances
implicit object MyTypeClassInstance extends MyTypeClassTemplate[Int] {
  override def doSomething(value: Int): String = ???
}
// Enriching types with type classes
implicit class ConversionClass[T](value: T) {
  def doSomething(implicit instance: MyTypeClassTemplate[T]): String = 
   instance.doSomething(value)
}