付録:様々な型クラスの紹介
本章ではImplicitの章で説明した型クラスの具体例を紹介します。本章で紹介する型クラスは、必ずしもScalaでのプログラミングに必要というわけではありません。しかし、世の中に存在するScalaで実装されたライブラリやアプリケーションのいくつかでは、本章で紹介する型クラスなどを多用している場合があります。そのようなライブラリやアプリケーションに出会った際にも臆さずコードリーディングができるよう、最低限の知識をつけることが本章の目的です。
本章で紹介する型クラスを絡めたScalaでのプログラミングについて詳しく知りたい場合はScala関数型デザイン&プログラミングを読みましょう。
Functor
前章に登場したList
やOption
には、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型クラスのインスタンスを定義し、前述の規則を満たすかどうか調べてみましょう。
scala> import scala.language.higherKinds
import scala.language.higherKinds
scala> trait Functor[F[_]] {
| def map[A, B](fa: F[A])(f: A => B): F[B]
| }
defined trait Functor
scala> def identityLaw[F[_], A](fa: F[A])(implicit F: Functor[F]): Boolean =
| F.map(fa)(identity) == fa
identityLaw: [F[_], A](fa: F[A])(implicit F: Functor[F])Boolean
scala> 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)
compositeLaw: [F[_], A, B, C](fa: F[A], f1: A => B, f2: B => C)(implicit F: Functor[F])Boolean
scala> implicit object OptionFunctor extends Functor[Option] {
| def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
| }
defined object OptionFunctor
scala> val n: Option[Int] = Some(2)
n: Option[Int] = Some(2)
scala> identityLaw(n)
res1: Boolean = true
scala> compositeLaw(n, (i: Int) => i * i, (i: Int) => i.toString)
res2: 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)))
また、ap
とpoint
を使って定義したmap
関数がFunctorのものと同じ振る舞いになることを確認する必要があります。
例として、Option型でApplicative Functorを定義してみましょう。
scala> 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))
| }
defined trait Applicative
scala> def identityLaw[F[_], A](fa: F[A])(implicit F: Applicative[F]): Boolean =
| F.ap(fa)(F.point((a: A) => a)) == fa
identityLaw: [F[_], A](fa: F[A])(implicit F: Applicative[F])Boolean
scala> 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))
homomorphismLaw: [F[_], A, B](f: A => B, a: A)(implicit F: Applicative[F])Boolean
scala> 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)))
interchangeLaw: [F[_], A, B](f: F[A => B], a: A)(implicit F: Applicative[F])Boolean
scala> 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
| }
| }
defined object OptionApplicative
scala> val a: Option[Int] = Some(1)
a: Option[Int] = Some(1)
scala> val f: Int => String = { i => i.toString }
f: Int => String = $$Lambda$34167/1094159196@215a3604
scala> val af: Option[Int => String] = Some(f)
af: Option[Int => String] = Some($$Lambda$34167/1094159196@215a3604)
scala> identityLaw(a)
res5: Boolean = true
scala> homomorphismLaw(f, 1)
res6: Boolean = true
scala> interchangeLaw(af, 1)
res7: Boolean = true
scala> OptionApplicative.map(a)(_ + 1) == OptionFunctor.map(a)(_ + 1)
res8: 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型が前述の規則をみたすかどうか確認してみましょう。
scala> trait Monad[F[_]] {
| def point[A](a: A): F[A]
| def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
| }
defined trait Monad
scala> def rightIdentityLaw[F[_], A](a: F[A])(implicit F: Monad[F]): Boolean =
| F.bind(a)(F.point(_)) == a
rightIdentityLaw: [F[_], A](a: F[A])(implicit F: Monad[F])Boolean
scala> def leftIdentityLaw[F[_], A, B](a: A, f: A => F[B])(implicit F: Monad[F]): Boolean =
| F.bind(F.point(a))(f) == f(a)
leftIdentityLaw: [F[_], A, B](a: A, f: A => F[B])(implicit F: Monad[F])Boolean
scala> 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))
associativeLaw: [F[_], A, B, C](fa: F[A], f: A => F[B], g: B => F[C])(implicit F: Monad[F])Boolean
scala> 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
| }
| }
defined object OptionMonad
scala> val fa: Option[Int] = Some(1)
fa: Option[Int] = Some(1)
scala> val f: Int => Option[Int] = { n => Some(n + 1) }
f: Int => Option[Int] = $$Lambda$34175/343775278@72d8055e
scala> val g: Int => Option[Int] = { n => Some(n * n) }
g: Int => Option[Int] = $$Lambda$34176/1972465973@6412882e
scala> rightIdentityLaw(fa)
res11: Boolean = true
scala> leftIdentityLaw(1, f)
res12: Boolean = true
scala> associativeLaw(fa, f, g)
res13: 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インスタンスを定義してみましょう。
scala> trait Monoid[F] {
| def append(a: F, b: F): F
| def zero: F
| }
defined trait Monoid
scala> def leftIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(F.zero, a)
leftIdentityLaw: [F](a: F)(implicit F: Monoid[F])Boolean
scala> def rightIdentityLaw[F](a: F)(implicit F: Monoid[F]): Boolean = a == F.append(a, F.zero)
rightIdentityLaw: [F](a: F)(implicit F: Monoid[F])Boolean
scala> 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))
| }
associativeLaw: [F](a: F, b: F, c: F)(implicit F: Monoid[F])Boolean
scala> 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
| }
defined object OptionIntMonoid
scala> val n: Option[Int] = Some(1)
n: Option[Int] = Some(1)
scala> val m: Option[Int] = Some(2)
m: Option[Int] = Some(2)
scala> val o: Option[Int] = Some(3)
o: Option[Int] = Some(3)
scala> leftIdentityLaw(n)
res14: Boolean = true
scala> rightIdentityLaw(n)
res15: Boolean = true
scala> associativeLaw(n, m, o)
res16: Boolean = true
型によっては結合方法が複数存在する場合があります。その際は複数のMonoidインスタンスを定義しておき、状況に応じて使いたいMonoidインスタンスを選択できるようにしておきましょう。
1. ここで出現するF
は、通常の型ではなく、「何かの型を受け取って、型を返すもの」で、型構築子、型コンストラクタなどと呼びます。List
やOption
は型構築子の一種です。詳細については、型システム入門 プログラミング言語と型の理論の第VI部「高階の型システム」を参照してください。 ↩