在代数中,就像在日常概念形成中一样,抽象是通过按一些基本特征对事物进行分组并省略其特定的其他特征而形成的。抽象统一在一个表示相似性的符号或单词下。我们说我们抽象差异,但这实际上意味着我们正在整合相似之处。
例如,考虑一个程序,它采用数字 1、2 和 3 的总和:
val sumOfOneTwoThree = 1 + 2 + 3
这个程序不是很有趣,因为它不是很抽象。我们可以抽象我们正在求和的数字,通过在单个符号ns 下整合所有数字列表:
def sumOf(ns: List[Int]) = ns.foldLeft(0)(_ + _)
而且我们也不特别关心它是否是一个列表。 List 是一个特定的类型构造函数(接受一个类型并返回一个类型),但我们可以通过指定我们想要的基本特征(它可以折叠)来抽象类型构造函数:
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
}
def sumOf[F[_]](ns: F[Int])(implicit ff: Foldable[F]) =
ff.foldl(ns, 0, (x: Int, y: Int) => x + y)
我们可以为List 和任何其他我们可以折叠的东西提供隐含的Foldable 实例。
implicit val listFoldable = new Foldable[List] {
def foldl[A, B](as: List[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}
implicit val setFoldable = new Foldable[Set] {
def foldl[A, B](as: Set[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}
val sumOfOneTwoThree = sumOf(List(1,2,3))
更重要的是,我们可以抽象操作和操作数的类型:
trait Monoid[M] {
def zero: M
def add(m1: M, m2: M): M
}
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
def foldMap[A, B](as: F[A], f: A => B)(implicit m: Monoid[B]): B =
foldl(as, m.zero, (b: B, a: A) => m.add(b, f(a)))
}
def mapReduce[F[_], A, B](as: F[A], f: A => B)
(implicit ff: Foldable[F], m: Monoid[B]) =
ff.foldMap(as, f)
现在我们有了一些非常普遍的东西。方法mapReduce 将折叠任何F[A],因为我们可以证明F 是可折叠的并且A 是一个幺半群或可以映射成一个。例如:
case class Sum(value: Int)
case class Product(value: Int)
implicit val sumMonoid = new Monoid[Sum] {
def zero = Sum(0)
def add(a: Sum, b: Sum) = Sum(a.value + b.value)
}
implicit val productMonoid = new Monoid[Product] {
def zero = Product(1)
def add(a: Product, b: Product) = Product(a.value * b.value)
}
val sumOf123 = mapReduce(List(1,2,3), Sum)
val productOf456 = mapReduce(Set(4,5,6), Product)
我们已经抽象了个幺半群和可折叠。