付録:様々な型クラスの紹介

本章ではImplicitの章で説明した型クラスの具体例を紹介します。本章で紹介する型クラスは、必ずしもScalaでのプログラミングに必要というわけではありません。しかし、世の中に存在するScalaで実装されたライブラリやアプリケーションのいくつかでは、本章で紹介する型クラスなどを多用している場合があります。そのようなライブラリやアプリケーションに出会った際にも臆さずコードリーディングができるよう、最低限の知識をつけることが本章の目的です。

本章で紹介する型クラスを絡めたScalaでのプログラミングについて詳しく知りたい場合はScala関数型デザイン&プログラミングを読みましょう。

Functor

前章に登場したListOptionには、mapという関数が共通して定義されていました。このmap関数がある規則を満たす場合はFunctor型クラスとして抽象化できます1

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

この型クラスが満たすべき規則は2つです。

def identityLaw[F[_], A](fa: F[A])(implicit F: Functor[F]): Boolean =
  F.map(fa)(identity) == fa

def compositeLaw[F[_], A, B, C](fa: F[A], f1: A => B, f2: B => C)(implicit F: Functor[F]): Boolean =
  F.map(fa)(f2 compose f1) == F.map(F.map(fa)(f1))(f2)

なお、identityは次のように定義されます。

def identity[A](a: A): A = a

例として、Option型でFunctor型クラスのインスタンスを定義し、前述の規則を満たすかどうか調べてみましょう。

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

def identityLaw[F[_], A](fa: F[A])(implicit F: Functor[F]): Boolean =
  F.map(fa)(identity) == fa

def compositeLaw[F[_], A, B, C](fa: F[A], f1: A => B, f2: B => C)(implicit F: Functor[F]): Boolean =
  F.map(fa)(f2 compose f1) == F.map(F.map(fa)(f1))(f2)

implicit object OptionFunctor extends Functor[Option] {
  def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}

val n: Option[Int] = Some(2)
// n: Option[Int] = Some(value = 2)
identityLaw(n)
// res0: Boolean = true
compositeLaw(n, (i: Int) => i * i, (i: Int) => i.toString)
// res1: Boolean = true

Applicative Functor

複数の値が登場する場合にはFunctorでは力不足です。そこで、複数の引数を持つ関数と値を組み合わせて1つの値を作りだせる機能を提供するApplicative Functorが登場します。

trait Applicative[F[_]] {
  def point[A](a: A): F[A]
  def ap[A, B](fa: F[A])(f: F[A => B]): F[B]
}

Applicative FunctorはFunctorを特殊化したものなので、Applicative Functorが持つ関数からmap関数を定義できます。

def map[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Applicative[F]): F[B] =
  F.ap(fa)(F.point(f))

Applicative Functorが満たすべき規則は以下の通りです。

def identityLaw[F[_], A](fa: F[A])(implicit F: Applicative[F]): Boolean =
  F.ap(fa)(F.point((a: A) => a)) == fa

def homomorphismLaw[F[_], A, B](f: A => B, a: A)(implicit F: Applicative[F]): Boolean =
  F.ap(F.point(a))(F.point(f)) == F.point(f(a))

def interchangeLaw[F[_], A, B](f: F[A => B], a: A)(implicit F: Applicative[F]): Boolean =
  F.ap(F.point(a))(f) == F.ap(f)(F.point((g: A => B) => g(a)))

また、appointを使って定義したmap関数がFunctorのものと同じ振る舞いになることを確認する必要があります。

例として、Option型でApplicative Functorを定義してみましょう。

trait Applicative[F[_]] {
  def point[A](a: A): F[A]
  def ap[A, B](fa: F[A])(f: F[A => B]): F[B]
  def map[A, B](fa: F[A])(f: A => B): F[B] = ap(fa)(point(f))
}

def identityLaw[F[_], A](fa: F[A])(implicit F: Applicative[F]): Boolean =
  F.ap(fa)(F.point((a: A) => a)) == fa

def homomorphismLaw[F[_], A, B](f: A => B, a: A)(implicit F: Applicative[F]): Boolean =
  F.ap(F.point(a))(F.point(f)) == F.point(f(a))

def interchangeLaw[F[_], A, B](f: F[A => B], a: A)(implicit F: Applicative[F]): Boolean =
  F.ap(F.point(a))(f) == F.ap(f)(F.point((g: A => B) => g(a)))

implicit object OptionApplicative extends Applicative[Option] {
  def point[A](a: A): Option[A] = Some(a)
  def ap[A, B](fa: Option[A])(f: Option[A => B]): Option[B] = f match {
    case Some(g) => fa match {
      case Some(a) => Some(g(a))
      case None => None
    }
    case None => None
  }
}

val a: Option[Int] = Some(1)
// a: Option[Int] = Some(value = 1)
val f: Int => String = { i => i.toString }
// f: Int => String = <function1>
val af: Option[Int => String] = Some(f)
// af: Option[Int => String] = Some(value = <function1>)
identityLaw(a)
// res2: Boolean = true
homomorphismLaw(f, 1)
// res3: Boolean = true
interchangeLaw(af, 1)
// res4: Boolean = true
OptionApplicative.map(a)(_ + 1) == OptionFunctor.map(a)(_ + 1)
// res5: Boolean = true

Monad

ある値を受け取りその値を包んだ型を返す関数をApplicative Functorで扱おうとすると、型がネストしてしまい平坦化できません。このネストする問題を解決するためにMonadと呼ばれる型クラスを用います。

trait Monad[F[_]] {
  def point[A](a: A): F[A]
  def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
}

bindはOptionやListで登場したflatMapを抽象化したものです。

Monadは以下の規則を満たす必要があります。

def rightIdentityLaw[F[_], A](a: F[A])(implicit F: Monad[F]): Boolean =
  F.bind(a)(F.point(_)) == a

def leftIdentityLaw[F[_], A, B](a: A, f: A => F[B])(implicit F: Monad[F]): Boolean =
  F.bind(F.point(a))(f) == f(a)

def associativeLaw[F[_], A, B, C](fa: F[A], f: A => F[B], g: B => F[C])(implicit F: Monad[F]): Boolean =
  F.bind(F.bind(fa)(f))(g) == F.bind(fa)((a: A) => F.bind(f(a))(g))

MonadはApplicative Functorを特殊化したものなので、Monadが持つ関数からpoint関数とap関数を定義できます。 pointに関しては同じシグネチャなので自明でしょう。

def ap[F[_], A, B](fa: F[A])(f: F[A => B])(implicit F: Monad[F]): F[B] =
  F.bind(f)((g: A => B) => F.bind(fa)((a: A) => F.point(g(a))))

それでは、Option型が前述の規則をみたすかどうか確認してみましょう。

trait Monad[F[_]] {
  def point[A](a: A): F[A]
  def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
}

def rightIdentityLaw[F[_], A](a: F[A])(implicit F: Monad[F]): Boolean =
  F.bind(a)(F.point(_)) == a

def leftIdentityLaw[F[_], A, B](a: A, f: A => F[B])(implicit F: Monad[F]): Boolean =
  F.bind(F.point(a))(f) == f(a)

def associativeLaw[F[_], A, B, C](fa: F[A], f: A => F[B], g: B => F[C])(implicit F: Monad[F]): Boolean =
  F.bind(F.bind(fa)(f))(g) == F.bind(fa)((a: A) => F.bind(f(a))(g))

implicit object OptionMonad extends Monad[Option] {
  def point[A](a: A): Option[A] = Some(a)
  def bind[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa match {
    case Some(a) => f(a)
    case None => None
  }
}

val fa: Option[Int] = Some(1)
// fa: Option[Int] = Some(value = 1)
val f: Int => Option[Int] = { n => Some(n + 1) }
// f: Int => Option[Int] = <function1>
val g: Int => Option[Int] = { n => Some(n * n) }
// g: Int => Option[Int] = <function1>
rightIdentityLaw(fa)
// res6: Boolean = true
leftIdentityLaw(1, f)
// res7: Boolean = true
associativeLaw(fa, f, g)
// res8: Boolean = true

Monoid

2つの同じ型を結合する機能を持ち、更にゼロ値を知る型クラスはMonoidと呼ばれています。

trait Monoid[F] {
  def append(a: F, b: F): F
  def zero: F
}

前章で定義したAdditive型とよく似ていますが、Monoidは次の規則を満たす必要があります。

def leftIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(F.zero, a)
def rightIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(a, F.zero)
def associativeLaw[F](a: F, b: F, c: F)(implicit F: Monoid[F]): Boolean = {
  F.append(F.append(a, b), c) == F.append(a, F.append(b, c))
}

Option[Int]型でMonoidインスタンスを定義してみましょう。

trait Monoid[F] {
  def append(a: F, b: F): F
  def zero: F
}

def leftIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(F.zero, a)
def rightIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(a, F.zero)
def associativeLaw[F](a: F, b: F, c: F)(implicit F: Monoid[F]): Boolean = {
  F.append(F.append(a, b), c) == F.append(a, F.append(b, c))
}

implicit object OptionIntMonoid extends Monoid[Option[Int]] {
  def append(a: Option[Int], b: Option[Int]): Option[Int] = (a, b) match {
    case (None, None) => None
    case (Some(v), None) => Some(v)
    case (None, Some(v)) => Some(v)
    case (Some(v1), Some(v2)) => Some(v1 + v2)
  }
  def zero: Option[Int] = None
}

val n: Option[Int] = Some(1)
// n: Option[Int] = Some(value = 1)
val m: Option[Int] = Some(2)
// m: Option[Int] = Some(value = 2)
val o: Option[Int] = Some(3)
// o: Option[Int] = Some(value = 3)
leftIdentityLaw(n)
// res9: Boolean = true
rightIdentityLaw(n)
// res10: Boolean = true
associativeLaw(n, m, o)
// res11: Boolean = true

型によっては結合方法が複数存在する場合があります。その際は複数のMonoidインスタンスを定義しておき、状況に応じて使いたいMonoidインスタンスを選択できるようにしておきましょう。

1. ここで出現するFは、通常の型ではなく、「何かの型を受け取って、型を返すもの」で、型構築子、型コンストラクタなどと呼びます。ListOptionは型構築子の一種です。詳細については、型システム入門 プログラミング言語と型の理論の第VI部「高階の型システム」を参照してください。

results matching ""

    No results matching ""