【函数式】Monads模式初探——Monoids

Monads是什么

知乎里有关于什么是Monad的问题讨论,而在维基百科中也有关于Monad的释义。作为初次接触到Monads概念,难免会有些晕头转向,也难免会有些畏惧(因为Monads和数学中的范畴论有密切关系),但是Monads又是如此的重要,因为它在函数式编程中实在是应用太广泛了,并且在Scala的标准库中又常常遇到,使得我们不得不好好研究一番。

Monoids

Monoids是一种元素的集合,它需要满足结合律和幺元(Identity,也称为单位元,这种元和其他元素结合时,不会改变那么元素)这些约束条件。
比如:

  • 整数类型Int,其中0是Identity,其中的任何整数满足结合律
  • 列表类型List,任何两个列表可以通过:::连接起来,其中Nil或空列表[]是Identity
  • 字符串类型String,两个字符串可以拼接,其中空字符串或”“是Identity

Monoids在平常的编程之中无处不在,当用到一个列表,连接字符串,通过一个循环得到一个累加结果,都在使用到Monoids。

条件和定律

  1. 一个抽象类型A
  2. 一个二元结合性函数(binary associative function),对传入的两个A类参数进行操作后产生一个A类型结果。op操作必须是结合性的,即op(x, y) == op(y, x);op(a,op(b,c)) = op(op(a,b),c):这个定律是函数组合(function composition)不可缺的条件
  3. 一个恒等值(identity)。二元函数参数中如果有一个是恒等值时操作结果为另一个参数,即满足op(identity, x) == x

示例

Monoid可以用下面的代码描述:

trait Monoid[T] {
  def op(m1: T, m2: T): T
  val identity: T
}

这个特质可以被混入类型(classes)、对象(objects)或者其他特质中。请看下面的举例:

case class StringMonoid extends Monoid[String] {
  def op(s1: String, s2: String) = s1 + s2
  val identity = ""
}

val stringMonoid = StringMonoid()
println(stringMonoid.op(stringMonoid.identity, "John"))
println(stringMonoid.op("John", "Hunt"))
// Output is
// John
// JohnHunt

object IntMonoid extends Monoid[Int] {
  def op(x: Int, y: Int) = x + y
  val identity = 0
}

println(IntMonoid.op(IntMonoid.identity, 1))
println(IntMonoid.op(1, 2))
println(IntMonoid.op(2, 1))
// Output is
// 1
// 3
// 3

Monoid和折叠

如果有一个Monoid结构和一组数据。可以通过对每个元素进行Monoid的op操作来将集合缩减为一个值,比如将一个整数列表通过元素累加的方式得到所有整数的和。
Monoid和List有着密切的联系。在List的foldLeft操作中,用一个初始元素从列表的左边元素开始操作,一直到对所有元素都操作完。如List("A", "B", "C").foldLeft("")(_ + _)这个对字符串列表实现累加功能,foldLeft传入的两个参数分别是空字符串和二元操作运算,这正好符合Monoid的定义,可以轻松利用StringMonoid代替,List("A", "B", "C").foldLeft(StringMonoid.identity)(StringMonoid.op)

结合性与并行化

Monoid的结合性意味着我们在对类似List的数据结构进行折叠的时候有很大的灵活性。我们已经知道可以使用foldLeft和foldRight对一个列表进行顺序的规则(reduce)操作。但是我们同样可以将数据分成多份,并行的进行折叠,然后利用monoid将各个部分合并起来。
左折叠操作是op(op(op(a, b), c), d)
右折叠操作是op(a, op(b, op(c, d)))
并行算法为op(op(a, b), op(c, d)),其中op(a, b)和op(c, d)是同时运算的。
如果我们对一个超大文件进行文字数统计或者寻找最大值什么的,我们可以把这个大文件分成若干小文件然后同时计算后再合计将节省很多计算时间。

Monoid模式的优缺点

优点:
- Monoid模式提供了一种在特定场景下将元素合并的标准方法
- 结合性的保证可以用来定义函数之间组合

缺点:
- 并不是所有集合都可以很容易的应用Monoid模式。比如String Monoid,不同顺序的字符串进行连接可能会得到不同的结果。

从范畴论到计算机编程

从Monoid到Monad,这些概念都是从范畴论中衍生出来的。
理解范畴论的一个好方法是把它理解为应用到函数式编程领域的设计模式。范畴论定义了一些非常底层的概念抽象,这些概念可以直接用Scala这样的支持函数式编程的语言表达。在设计软件的时候,如果一个特定实体符合其中一个概念,那么立刻就有一整组操作可用,而且包含推理其用法的方法。

范畴是由元素对象和态射箭头组成的,这个箭头开始端是一个元素对象,目的地也是一个元素对象。这里态射箭头有两种,不同元素对象比如a和b之间的态射箭头称为组合箭头,而指向自己的箭头称为元箭头,或者单元,幺元。

范畴的元素对象和箭头态射的规则如下:

  1. 对于箭头f:a -> b和箭头g:b -> c,如果有一个箭头h: a -> c,那么就称为它们的组合,写法是:h=g·f
  2. 对于每个元素对象,都有一个单元箭头:id:a -> a。对于任何f: a -> b,满足f·id = f;对于任何g: c -> a,满足id·g = g
  3. 组合符合结合律:f·(g·h) = (f·g)·h
    态射箭头有两种,一种是标号1的组合箭头,还有一种是标号2的单元箭头。

我们将一个范畴有元素对象和态射箭头,态射箭头有组合和幺元两种,且满足结合律,这种范畴称为Monoid。

对于某非空集合S,若存在S上的二元运算”*”使得对于任意的a,b∈S,有a*b∈S(运算封闭),则称{S,*}为广群。 广群只是定义一个集合,集合中有元素和操作,操作结果也属于这个集合,这样泛泛的集合称为广群。 如果广群再加上结合律约束,就会得到半群,因此半群是广群的子集,要求更苛刻些,而半群如果再加上幺元(identity element)就是幺半群,也就是结合律+幺元=幺半群,所以,Monid对应的中文是幺半群。

参考资料

Functional Programming in Scala
Scala Design Patterns: Patterns for Practical Reuse and Design
什么是Monoid?
我所理解的monad(2):fold与monoid

转载请注明作者Jason Ding及其出处
Github博客主页(http://jasonding1354.github.io/)
GitCafe博客主页(http://jasonding1354.gitcafe.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354进入我的博客主页

你可能感兴趣的:(【Scala,Programming】,Scala编程实例)