在实际函数式编程中并不怎么提及 Semigroupal
,因为 Semigroupal
仅提供了另一个 type class,即 Applicative Functor 功能的 子集。
实际上 Semigroupal
和 Applicative
是 同一个概念的不同表达方式,该概念即 join context
。
Cats 使用两个 type class 对 applicative 建模:
cats.Apply
Apply
继承Semigroupal
和Functor
Apply
添加ap
函数- 类型为
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
- 将
function in context
应用到value in context
- 类型为
cats.Applicative
Applicative
继承Apply
,并添加pure
函数
下面是 Apply
和 Applicative
在 Cats 中的简要定义:
trait Apply[F[_]] extends Semigroupal[F] with Functor[F] {
def ap[A, B](ff: F[A ⇒ B])(fa: F[A]): F[B]
def product[A, B](fa: F[A], fb: F[B]) =
ap(map(fa)(a => (b: B) => (a, b)))(fb)
}
trait Applicative[F[_]] extends Apply[F] {
def pure[A](a: A): F[A]
}
product
函数是通过 ap
+ map
实现的,实际上 product
ap
和 map
联系非常紧密,任意一方都能通过剩余两个函数定义!
Applicative
特质仅在Apply
上添加了pure
函数Monoid
特质仅在Semigroup
上添加了empty
函数
并且 pure
empty
概念非常类似!
引入 Apply
和 Applicative
后,基本覆盖了以不同方式进行 sequencing computations 的各个 type class,他们之间关系如下:
上图中的每个 type class 都:
- 代表一种独特的 sequencing semantics
- 定义辨识度很高的独特方法,并用这些方法实现其父类中的方法
例如:
Foo
是一个 monad,意味着:- 有一个
Monad[Foo]
实例 - 且该实例实现了
pure
和flatMap
- 且该实例继承了
ap
product
和map
的标准定义
- 有一个
Bar
是一个 applicative,意味着:- 有一个
Applicative[Bar]
实例 - 且该实例实现了
pure
和ap
- 且该实例继承了
product
和map
- 有一个
仅凭 Foo
是 monad,而 Bar
是 applicative,能推测出什么呢?
- 因为
Monad
是Applicative
的子类,所以Foo
具备flatMap
,而Bar
没有 - 因为
Applicative
约束更少,所以Bar
可以比Foo
更加灵活
这引出了 power 和 constraint 的权衡:
- 对数据类型施加的 *constraints 越多,则其行为 更加可预期(例如
flatMap
) - 但更多约束,也意味着该数据类型能建模的行为 更少,即其 power 更弱
而 Monad
恰好是一个很好折中点,能力够强,可以对很多行为建模,而约束也足够,从而对其行为有足够保证,当然也有一些 Monad
不适合的场景:
Monad
对计算施加 strict sequencing,适用于顺序计算Applicative
没有 顺序约束,因此可以用于并行/并发计算,但却丧失了flatMap
这个大杀器