cats - Functor
Functor
Functor
是用来表示抽象化出可以调用 map 方法的结构 的 类型类. 这种结构比如说有 List
, Option
, 和 Future
.
实现了 Functor 的类型, 可以看成是某种容器 或者是上下文, 而 map 方法就是表示要把容器中的东西变换成另外一种东西. map 方法的参数是 一个函数, 用来将 容器中的 A 类型转换成 B类型. 所以对于要实现 Functor 的类来说, 必须至少拥有一个类型参数, 来作为其内部的类型.
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Example implementation for Option
implicit val functorForOption: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
case None => None
case Some(a) => Some(f(a))
}
}
Functor 实例必须遵循两个规则:
-
结合律:
- fa.map(f).map(g) = fa.map(f.andThen(g))
类似于函数里面的 f(g(x)) = g(f(x))
-
同一律:
- fa.map(x => x) = fa
x => x, 也就是直接返回参数的函数称为 id (单位函数)
不同的角度
另一种看待 Functor[F] 的角度是, F允许从一个函数 A => B 提升(lift) 为一个新的函数 F[A] => F[B], 可以看看 lift 的函数签名:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
def lift[A, B](f: A => B): F[A] => F[B] =
fa => map(fa)(f)
}
lift 方法看起来有点抽象, 把函数签名提取出来, 就是 我们已知某个上下文/容器 F, 还有 A => B 的函数, 然后我们就能够组合出 F[A] => F[B] 的函数
用 Functor 来管理效果(effect)
Functor
中的 F
常常被称为 “效果” 或者 “计算上下文”. 不同的效果将抽象出不同的行为, 尽管我们都是调用 map. 比如, Option
的效果就是将潜在的缺失值给抽象出来, 当对一个 Some (Option 的 子类) 调用 map 的时候, 参数中的函数会生效, 但是对于 None (Option 的 另一个子类) 的情况来说, 就会直接跳过.
使用这种观点, 我们可以把 Functor
视为具有拥有处理单个效果的能力 - 我们可以把一个纯函数应用到某个值上面而无需将其剥离其上下文.
举个例子, 对于 List(1,2,3), 要变成 List(2,4,6). 普通 Java 的做法是新建一个 List, 然后遍历1 2 3, 分别 * 2 然后放到新的 List 中. 在 Scala 中 可以使用 List(1,2,3).map( i => i * 2) 来实现, 这样我们就无需将元素从List拿出来, 再放回去这样操作. 当然 Java 以后也支持 stream 上的 map 操作, 一样道理.
Functor 的组合
如果你曾经使用过类似于 Option[List[A]]
或者 List[Either[String, Future[A]]]
的结构并尝试调用其 map, 就会遇到 类似于 _.map(_.map(_.map(f)))
这样的窘境. 这就引申出了, Functor 的组合, 当 F
和 G
都拥有 Functor
的实例, 则 F[G[_]] 也拥有 Funtor
的实例.
这种组合可以通过 Functor#compose
来获得
也就是说 我们有两个类型F,G , 都拥有 Functor 的实例, 那么通过
Functor#compose
方法, 我们可以将其组合起来以获取F[G[_]]
的 Functor 实例, 这样我们就不用重新写一个实现了.
import cats.Functor
import cats.implicits._
val listOption = List(Some(1), None, Some(2))
// listOption: List[Option[Int]] = List(Some(1), None, Some(2))
// Through Functor#compose
Functor[List].compose[Option].map(listOption)(_ + 1)
// res1: List[Option[Int]] = List(Some(2), None, Some(3))
这种方法允许我们使用 Functor 组合而不需要层层调用map, 但是在复杂的用例下可能会引入复杂性. 比如我们想调用一个依赖某个 Functor 类型的函数, 并且我们想要使用 组合的Functor 作为参数, 我们就不得不显示地传递 组合的实例 在函数调用的时候或者创建一个本地的隐式变量.
def needsFunctor[F[_]: Functor, A](fa: F[A]): F[Unit] = Functor[F].map(fa)(_ => ())
def foo: List[Option[Unit]] = {
val listOptionFunctor = Functor[List].compose[Option]
type ListOption[A] = List[Option[A]]
needsFunctor[ListOption, Int](listOption)(listOptionFunctor)
}
这种情况下, 我们就可以使用 Nested
数据类型
import cats.data.Nested
import cats.implicits._
val nested: Nested[List, Option, Int] = Nested(listOption)
// nested: Nested[List, Option, Int] = Nested(List(Some(1), None, Some(2)))
nested.map(_ + 1)
// res2: Nested[List, Option, Int] = Nested(List(Some(2), None, Some(3)))
使用 Nested
方式, 另一个不同类型的组合类, 以通常的方式来解决可能的 SI-2712 问题 (也可以通过 部分一致化(partial unification) 来解决) , 但是具有语法和运行时封装和解封装的开销.
官网的整个解释还是挺抽象的, 入门还是建议去看看那本 《Cats with Scala》. 再搭配 《Haskell趣学指南》 食用更佳
参考
官方文档 https://typelevel.org/cats/typeclasses/functor.html