引き続き、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: f
とg
という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 Functor
はcontramap
というメソッドを提供する。
このcontramap
メソッドは変換を表すデータ型にのみ意味がある。
F[B] contramap (A => B) => F[A]
Invariant functors and the imap method
Invariant Functor
はimap
メソッドを実装しており、これはざっくり言うとmap
とcontramap
の組み合わせと同じ。
F[A] imap (A => B, B => A) => F[B]