bussorenre Laboratory

hoge piyo foo bar

Scala で Functor っぽいものを実際に実装して挙動を追ってみる

こんにちは。 @bussorenre です。今日はScala です。 定型句となって恐縮ですが、「ここは表現がおかしい」とか「間違ってる」とか「もっとこうしたほうがいい」等ありましたら、Twitter で罵倒する前にコメントをいただけると凄く助かります。。。

はじめに

Functor を自分なりの理解で書いてみます。「実際実装してみて挙動を追ってみるか」と言うスタンスの補足解説的な記事です。 eed3si9n さんの独習Scalaz のページを読み解きながら書いたので、先にこちらをご覧になることをオススメします。

eed3si9n.com

また、そもそもFunctor とはなんぞやという解説記事は以下の翻訳ページが参考になりました。

qiita.com

このページでは、上述のページを読んでいて「ん?」ってなったところを、可能な限りScalaで書いて理解を深めていこうと思います。

map

Functor は日本語で関手(かんしゅ)と呼ばれます。Scala で書くとこんな感じです。

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

FunctorF[A]F[B] に変換するmap 関数を定義します。要するにmap が定義できれば Functorということかな…?。入力値 F[A]A => Bに変換する関数を定義します。A と Bは同型でも問題なさそう。Fだと分かりづらいので、具体的な型で実際に例を見てみましょう。以下の例は、F = Option の場合の実装です。

def map[A, B](fa: Option[A])(f:A => B): Option[B] = {
  fa match {
    case Some(a) => Some(f(a))
    case None => None
  }
}

Option の中身が Some なら中の値に対して関数fを適応し、中身が Noneならそのまま Noneを返しています。簡単ですね。

F = Either の場合はこんな感じになりますかね…?

def map[A, B, C](fa: Either[A, B])(f:B => C): Either[A, C] = {
  fa match {
    case Left(a) => Left(a)
    case Right(b) => Right(f(b))
  }
}

この例では右側だけをmap していますが、左側だけをmap する leftMap などもScalaz.\/ には定義されています。

関数へのFunctor

さて、独習Scalaz を読むと Scalaz は Function1 に対する Functor のインスタンスも定義する。 という記述があります。下記コードもそこからの抜粋。

scala> ((x: Int) => x + 1) map {_ * 7}
res30: Int => Int = <function1>

scala> res30(3)
res31: Int = 28

さてどういうことだろうか?と思って、実際にScalaz のFunction.scala を見に行きます。

override def map[A, B](fa: () => A)(f: A => B) = () => f(fa())

Scalaz のどこかに, () を 「なんでも引数指定可な関数」みたいな意味の拡張があるんだろうか…?それともどこかに別の定義があるんだろうか?とにかく、関数のFunctor は、関数の合成を表すことが出来る。 A → B → C という順番で処理を実行してくれる A => C という関数を定義出来るみたいだ…。

lift

「持ち上げ」などと言うが、コレは何をやっているんだろうか? 実際に定義を見に行くとこんな感じになっています。

/** Lift `f` into `F`. */
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)

A => B と引数にとって、 F[A] => F[B] という関数を返しています。先程のOption の具体例を引き続き使うと、以下のような感じ

val f = (x: Int) => x + 100

def map[A, B](fa: Option[A])(f:A => B): Option[B] = {
  fa match {
    case Some(a) => Some(f(a))
    case None => None
  }
}

def lift[A, B](f: A => B): Option[A] => Option[B] = map(_)(f)

lift(f)
res0: Option[Int] => Option[Int] = $$Lambda$910/314622131@5a97b17c

res0(Some(100))
res1: Option[Int] = Some(200)

Int => Int という関数を Option[Int] => Option[Int] という関数に変換されました。これにより、先程定義した Int => Int という変換が、Option という文脈で包まれて登場しても問題なく利用できるということになります。 これ、渡すのがOption ではなく関数gだった場合、関数g が一つの文脈 という事になるんだろうか…?

まとめ

Functor の実装を追いながら、実際にOption という具体例で動作を追ってみた。

  • def map(fa: F[A])(f: A => B): F[B]
  • override def map[A, B](fa: () => A)(f: A => B) = () => f(fa())
  • def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)

個人的にOption がわかりやすいのでOption を使ったけど、 Either でも List でも何でも良いと思う。