Functors

引き続き、Scala with Catsを読んでいく。

Functors

Functorとはざっくり言うと、map メソッドを持ったものと言える。

通常、Listsの反復処理でmapメソッドに出会うが、Functorを理解するには別の視点で考える必要がある。

Listsを走査する」と考えるのではなく、「すべての値を一度に変換する」と考えるべきである。

ある関数を適用するとすべての値に適用され値は変化するがListsの構造は保ったままである。

List(1, 2, 3).map(n => n + 1)
// List(2, 3, 4)

Future

Futureは非同期計算をキューイングしてシーケンス化し、前の計算が完了した時点で適用するFunctorである。

Futureは内部の状態については保証せず、Futureでラップされた計算は、ongoing(進行中), complete(完了), rejected(拒否)で表される。

もし、Futureが完了した場合は、マッピングされたカンスは即座に呼ばれる。もしそうでない場合は、下位のスレッドプールが関数の呼び出しをキューに入れて後で呼び出される。

Futureにおいて、「関数がいつ呼び出されるかは分からない」が、「関数がどの順序で呼び出される」かは分かる。

Note: Futureはreferentially transparent(参照透過性)でない 。

Futureは常に計算結果をキャッシュするため、これらの動作を調整することはできない。

Definition of a Functor

Functorの型は F[A]で、map操作は (A => B) => F[B]

Functor Laws

Functorは、多くの小さな操作を一つずつ順番に行う場合でも、それらを組み合わせて大きな関数にしてからマッピングする場合でも同じセマンティクスを保証する。

Identity: identity関数でmapを呼び出しても、何もしていない場合と同じである

fa.map(a => a) == fa

Composition: fgという2つの関数をマッピングすることはfという関数をマッピングしてからgという関数をマッピングするのと同じである

fa.map(g(f(_))) == fa.map(f).map(g)

Functors in Cats

Functorの型クラスは cats.Functorである。

Functorはliftというメソッドを提供していて、これは A => B という型の関数をFunctor上で操作し、F[A] => F[B]という型に変換する関数。

val func = (x: Int) => x + 1
val liftedFunc = Functor[Option].lift(func)
liftedFunc(Option(1))
// Some(2)

Exercise: Branching out with Functors

2分木型のFunctorを書け、という問題。

以下は、2分木の定義。

sealed trait Tree[+A]
final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
final case class Leaf[A](value: A) extends Tree[A]

Functorの実装はすぐ書けた。

  
  implicit def binaryTreeFunctor: Functor[Tree] = new Functor[Tree] {
    override def map[A, B](fa: Tree[A])(f: A => B): Tree[B] =
      fa match {
        case Branch(left, right) => Branch(map(left)(f), map(right)(f))
        case Leaf(value) => Leaf(f(value))
      }
  }

これを実行してみようと以下のように書いてみると、Cannot resolve symbol map だと怒られた。

  println(Branch(Leaf(1), Branch(Leaf(2), Leaf(3))).map(_ * 2))

Cats.Functorの定義を見てみると以下のようになっており、Invariant[F]を継承している。

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

ということは、変位指定は不変なので、Tree型を継承したBranch型やLeaf型は渡せない。

なので、Treeオブジェクトを作成して、Branch型とLeaf型のコンストラクターを作成する。

object Tree {
  def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
    Branch(left, right)

  def leaf[A](value: A): Tree[A] = Leaf(value)
}

これらのコンストラクターの返り値は Tree[A] なので、怒られずに実行できるはず。

println(Tree.branch(Tree.leaf(1), Tree.branch(Tree.leaf(2), Tree.leaf(3))).map(_ * 2))
// Branch(Leaf(2),Branch(Leaf(4),Leaf(6)))

実行できた。

Contravariant Functors and the contramap Method

Contravariant Functorcontramapというメソッドを提供する。

このcontramapメソッドは変換を表すデータ型にのみ意味がある。

F[B] contramap (A => B) => F[A]

Invariant functors and the imap method

Invariant Functorimapメソッドを実装しており、これはざっくり言うとmapcontramapの組み合わせと同じ。

F[A] imap (A => B, B => A) => F[B]