型クラスの紹介

本章では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型クラスのインスタンスを定義し、前述の規則を満たすかどうか調べてみましょう。

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)))

また、appointを使って定義した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$2278/940394193@58f4672d

scala> val af: Option[Int => String] = Some(f)
af: Option[Int => String] = Some($$Lambda$2278/940394193@58f4672d)

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$2286/32120288@7ae0fffb

scala> val g: Int => Option[Int] = { n => Some(n * n) }
g: Int => Option[Int] = $$Lambda$2287/1638443217@5339915b

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

results matching ""

    No results matching ""