LYAHFGG:
今度は、
Functor
(ファンクター)という型クラスを見ていきたいと思います。Functor
は、全体を写せる (map over) ものの型クラスです。
本のとおり、実装がどうなってるかをみてみよう:
/**
* Functor.
*
* The name is short for "covariant functor".
*
* Must obey the laws defined in cats.laws.FunctorLaws.
*/
@typeclass trait Functor[F[_]] extends functor.Invariant[F] { self =>
def map[A, B](fa: F[A])(f: A => B): F[B]
....
}
このように使うことができる:
import cats._, cats.syntax.all._
Functor[List].map(List(1, 2, 3)) { _ + 1 }
// res0: List[Int] = List(2, 3, 4)
このような用例は関数構文と呼ぶことにする:
@typeclass
アノテーションによって自動的に map
関数が map
演算子になることは分かると思う。 fa
の所がメソッドの this
になって、第2パラメータリストが、
map
演算子のパラメータリストとなる:
// 生成されるコードの予想
object Functor {
trait Ops[F[_], A] {
def typeClassInstance: Functor[F]
def self: F[A]
def map[B](f: A => B): F[B] = typeClassInstance.map(self)(f)
}
}
これは、Scala collection ライブラリの map
とかなり近いものに見えるが、
この map
は CanBuildFrom
の自動変換を行わない。
Cats は Either[A, B]
の Functor
インスタンスを定義する。
(Right(1): Either[String, Int]) map { _ + 1 }
// res1: Either[String, Int] = Right(value = 2)
(Left("boom!"): Either[String, Int]) map { _ + 1 }
// res2: Either[String, Int] = Left(value = "boom!")
上のデモが正しく動作するのは現在の所 Either[A, B]
には標準ライブラリでは
map
を実装してないということに依存していることに注意してほしい。
例えば、List(1, 2, 3)
を例に使った場合は、
Functor[List]
の map
ではなくて、
リストの実装の map
が呼び出されてしまう。
そのため、演算子構文の方が読み慣れていると思うけど、
標準ライブラリが map
を実装していないことを確信しているか、
多相関数内で使うか以外は演算子構文は避けた方がいい。
回避策としては関数構文を使うことだ。
Cats は Function1
に対する Functor
のインスタンスも定義する。
{
val addOne: Int => Int = (x: Int) => x + 1
val h: Int => Int = addOne map {_ * 7}
h(3)
}
// res3: Int = 28
これは興味深い。つまり、map
は関数を合成する方法を与えてくれるが、順番が f compose g
とは逆順だ。通りで Scalaz は map
のエイリアスとして ∘
を提供するわけだ。Function1
のもう1つのとらえ方は、定義域 (domain) から値域 (range) への無限の写像だと考えることができる。入出力に関しては飛ばして Functors, Applicative Functors and Monoids へ行こう (本だと、「ファンクターからアプリカティブファンクターへ」)。
ファンクターとしての関数 …
ならば、型
fmap :: (a -> b) -> (r -> a) -> (r -> b)
が意味するものとは?この型は、a
からb
への関数と、r
からa
への関数を引数に受け取り、r
からb
への関数を返す、と読めます。何か思い出しませんか?そう!関数合成です!
あ、すごい Haskell も僕がさっき言ったように関数合成をしているという結論になったみたいだ。ちょっと待てよ。
ghci> fmap (*3) (+100) 1
303
ghci> (*3) . (+100) $ 1
303
Haskell では fmap
は f compose g
を同じ順序で動作してるみたいだ。Scala でも同じ数字を使って確かめてみる:
(((_: Int) * 3) map {_ + 100}) (1)
// res4: Int = 103
何かがおかしい。fmap
の宣言と Cats の map
関数を比べてみよう:
fmap :: (a -> b) -> f a -> f b
そしてこれが Cats:
def map[A, B](fa: F[A])(f: A => B): F[B]
順番が逆になっている。これに関して Paolo Giarrusso (@blaisorblade) 氏が説明してくれた:
これはよくある Haskell 対 Scala の差異だ。
Haskell では、point-free プログラミングをするために、「データ」の引数が通常最後に来る。例えば、
map f list
という引数順を利用してmap f . map g . map h
と書くことでリストの変換子を得ることができる。 (ちなみに、map は fmap を List ファンクターに限定させたものだ)一方 Scala では、「データ」引数はレシーバとなる。 これは、しばしば型推論にとっても重要であるため、map を関数のメソッドとして定義するのは無理がある。 Scala が
(x => x + 1) map List(1, 2, 3)
の型推論を行おうとするのを考えてみてほしい。
これが、どうやら有力な説みたいだ。
LYAHFGG:
fmap
も、関数とファンクター値を取ってファンクター値を返す 2 引数関数と思えますが、そうじゃなくて、関数を取って「元の関数に似てるけどファンクター値を取ってファンクター値を返す関数」を返す関数だと思うこともできます。fmap
は、関数a -> b
を取って、関数f a -> f b
を返すのです。こういう操作を、関数の持ち上げ (lifting) といいます。
ghci> :t fmap (*2)
fmap (*2) :: (Num a, Functor f) => f a -> f a
ghci> :t fmap (replicate 3)
fmap (replicate 3) :: (Functor f) => f a -> f [a]
パラメータ順が逆だということは、この持ち上げ (lifting) ができないということだろうか?
幸いなことに、Cats は Functor
型クラス内に派生関数を色々実装している:
@typeclass trait Functor[F[_]] extends functor.Invariant[F] { self =>
def map[A, B](fa: F[A])(f: A => B): F[B]
....
// derived methods
/**
* Lift a function f to operate on Functors
*/
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
/**
* Empty the fa of the values, preserving the structure
*/
def void[A](fa: F[A]): F[Unit] = map(fa)(_ => ())
/**
* Tuple the values in fa with the result of applying a function
* with the value
*/
def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] = map(fa)(a => a -> f(a))
/**
* Replaces the `A` value in `F[A]` with the supplied value.
*/
def as[A, B](fa: F[A], b: B): F[B] = map(fa)(_ => b)
}
見ての通り、lift
も入っている!
{
val lifted = Functor[List].lift {(_: Int) * 2}
lifted(List(1, 2, 3))
}
// res5: List[Int] = List(2, 4, 6)
これで {(_: Int) * 2}
という関数を List[Int] => List[Int]
に持ち上げることができた。
他の派生関数も演算子構文で使ってみる:
List(1, 2, 3).void
// res6: List[Unit] = List((), (), ())
List(1, 2, 3) fproduct {(_: Int) * 2}
// res7: List[(Int, Int)] = List((1, 2), (2, 4), (3, 6))
List(1, 2, 3) as "x"
// res8: List[String] = List("x", "x", "x")
LYAHFGG:
すべてのファンクターの性質や挙動は、ある一定の法則に従うことになっています。 … ファンクターの第一法則は、「
id
でファンクター値を写した場合、ファンクター値が変化してはいけない」というものです。
Either[A, B]
を使って確かめてみる。
val x: Either[String, Int] = Right(1)
// x: Either[String, Int] = Right(value = 1)
assert { (x map identity) === x }
第二法則は、2つの関数
f
とg
について、「f
とg
の合成関数でファンクター値を写したもの」と、「まずg
、次にf
でファンクター値を写したもの」が等しいことを要求します。
言い換えると、
val f = {(_: Int) * 3}
// f: Int => Int = <function1>
val g = {(_: Int) + 1}
// g: Int => Int = <function1>
assert { (x map (f map g)) === (x map f map g) }
これらの法則は Functor の実装者が従うべき法則で、コンパイラはチェックしてくれない。