Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Functor,Applicative Functor について勉強したので整理してみる

最近 Functional Programming in Scala の勉強会をずっとしているのですが,ようやく12章の Applicative Functor に入りました.ところが,急に登場した Applicative という概念がいまいち勉強会の時点ではつかめておらず,少し頭の中を整理したいと思ったので簡単にまとめてみようと思います.あまり体系的にまとめるつもりはありません.自身の理解のメモとしてインターネットに放流しておく予定です.

(Applicative Functor はアプリカティブファンクタと以下書きます.日本語ググラビリティのためにそうします.他のこれ関連の用語も同様にカタカナで表記します.)

なお,モノイドやモナドは登場させません.これには意図があって,『すごい Haskell たのしく学ぼう!』という本では,モナドの説明なしにアプリカティブファンクタの説明をじっくり行っていたためです.この説明はわかりやすいなと思いました.今回はこの本の11章を参照しながらいろいろと考えていきます.

勉強会で使用している本はこちら.

すごいHaskellたのしく学ぼう.これは関数型プログラミングの理解の助けになる本だなと思います.

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

コードそのものは Scala で書きます.すごい Haskell 本のソースコードを読みつつ,Scala で書くとこうなるかなと翻訳して書いています.ただ厳密にはちょっと違いそう.

ファンクタとは

ファンクタとは「関数で写せるもの」のことを言います.要するに写像 (map) です.といったところで,???が頭に浮かぶかもしれません.さっそくですが実装を見てみましょう.

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

これを見ると一目瞭然なのですが,F の中身の型を A から B に変えているものですね.F にはたとえば OptionList といった型が入ります.この外側の型を保ちながら,内側の型を別の型に変換することをしてくれる抽象的な概念をファンクタと呼びます.難しくいうと,値があるかないかという Option の文脈を保ちながら,型を変換しているということです.

ファンクタは1引数しか受け取ることができません.なので,Either のような2つの型変数をもつ型をファンクタにするには,一度部分適用をして,あと1つ型変数を引数に取るだけの状態にする必要があります.

さて,ファンクタのざっくりとした説明はここまでです.11章では lift などの概念も実は登場してきていますが,今回は省きました.

アプリカティブファンクタとは

さて,欲しくなる場面ですが,たとえば次のようなコードがあったとしてみてください.

val f = n => n * 3
val someF = Some(f)
val some5 = Some(5)
// いい感じに Some(f(5)) をしたい

Some(f) の f に Some(5) の5を適用したいとなった場合にどうしたらよいのでしょうか.普通のファンクタを扱う限り,これはちょっと難しそうです.というのも,普通のファンクタでできるのは,「通常の関数で」「ファンクタの中の値を」写すことだけだからです.上述したように,一度 Some の中の値を両方とも取り出して,再度 Some に詰め直すという作業をするか,あるいは fmap を実装しているのであればそれを2回適用すればよさそうに見えます.

ただ,もう少しスマートにやる方法があります.それがアプリカティブファンクタという概念です.コードを見てみましょう.

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](a: => A): F[A]
  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B]
}

要するに,Fの中に入った関数を元の F に適用 (apply) して,その結果値を受け取ることができるものです.それゆえに applicative …?ただ,こうすることで,ファンクタでは1引数関数でしか処理できなかったものを,アプリカティブファンクタでは2引数関数で処理できるようになります.これはファンクタよりも強力になったことを意味します.

pure というのは,なんでもない a という値を受け取ると,それを F の中に入れて返すものです.<*> は,見たとおりで F という型の中で関数を適用してくれるスグレモノです.

ところで,Haskell の Applicative にはさらに便利な関数があるようです.それが,liftA2 という関数です.liftA2 は,「通常の2引数関数を,2つのアプリカティブ値を引数に取る関数に昇格させる」という機能を持っています.実装を見てみましょう.

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](a: => A): F[A]
  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B]
  def liftA2[A, B, C](a: F[A], b: F[B], f: (A, B) => C): F[C] // ←追加された
}

実装のとおりです.ここまでそろうと,ようやくアプリカティブファンクタを実装することができます.実装してみました.

trait Applicative[F[_]] extends Functor[F] {

  def pure[A](a: => A): F[A]

  override def fmap[A, B](fa: F[A], f: A => B): F[B] = this(fa, pure(f))

  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B] =
    liftA2(f, fa, (_: A => B)(_: A))

  private def apply[A, B](fa: => F[A], f: F[A => B]): F[B] = <*>(fa, f)

  def liftA2[A, B, C](a: F[A], b: F[B], f: (A, B) => C): F[C] =
    this(b, fmap(a, f.curried))
}

コンパイルも通ったしたぶん大丈夫…!private defapply を追加したのは,単純に <*> を使おうとするとうまいこと中置記法できなくてかっこ悪いと思っただけです.あんまり深い意図はありません.

ところで <*>fmapliftA2 が循環参照しているので,最終的にこの Applicative を使う側でどこかを実装してやる必要があるのでしょうか.Functional Programming in Scala の中でも循環参照している実装例が出てきたのですが,使う側で個別実装したら大丈夫だったので,たぶんそういうことなのだと思っています.

ということで,少し実装をしてみましょう.たとえば Maybe という型があったとします (Haskell から拝借).

sealed trait Maybe[+A] {
  def <*>[B](f: Maybe[A => B])(implicit F: Applicative[Maybe]): Maybe[B] = F <*> (this, f)
}

object Maybe {
  case class Just[+A](a: A) extends Maybe[A]
  case object Nothing extends Maybe[Nothing]
}

object applicatives {
  implicit def maybeApplicative: Applicative[Maybe] = new Applicative[Maybe] {
    override def pure[A](a: => A): Maybe[A] = Just(a)
    override def fmap[A, B](fa: Maybe[A], f: A => B): Maybe[B] = fa match {
      case Just(a) => pure(f(a))
      case Nothing => Nothing
    }
    override def <*>[A, B](fa: => Maybe[A], f: Maybe[A => B]): Maybe[B] =
      f match {
        case Just(something) => fmap(fa, something)
        case Nothing         => Nothing
      }
  }
}

これであっているのかは若干怪しいところですが (ちょっと美しくない実装な気もする),おおよそのイメージは掴んでもらえるはずです.実行して試してみましょう.

object Main extends App {
  val just5 = Just(5)
  val justF = Just((n: Int) => n * 3)
  implicit val maybeApplicative = implicitly[Applicative[Maybe]](applicatives.maybeApplicative)
  println(just5 <*> justF)
}

これを実行すると,結果は

> Just(15)

と返ってきます.やりたかったことが実現できましたね.

まとめ

  • ファンクタは map のことであり,関数の写しである.F の中身を単に取り出して変換する.
  • アプリカティブファンクタは Haskell だと <*> であり,2つの F を受け取って,両者を引数に受け取る関数を適用して結果値を取り出したり,あるいは,F の中に入った関数を F の中で適用して結果値を受け取ることができる.
    • まとまながらなるほどなと思ったんですけど,これがゆえに Functional Programming in Scala では Traverse[F[_]] の説明に入ってゆくのですね.

しかし,とくにアプリカティブファンクタの方がまだまだ理解が100%になった気はしませんね.つかめるまでもう少しエクササイズをしたいと思います.