泛函编程(21)-泛函数据类型-Monoid

    Monoid是数学范畴理论(category theory)中的一个特殊范畴(category)。不过我并没有打算花时间从范畴理论的角度去介绍Monoid,而是希望从一个程序员的角度去分析Monoid以及它在泛函编程里的作用。从这个思路出发我们很自然得出Monoid就是一种数据类型,或者是一种在泛函编程过程中经常会遇到的数据类型:当我们针对List或者loop进行一个数值的积累操作时我们就会使用到Monoid。实际上Monoid就是List[A] => A的抽象模型。好了,我们就不要越描越黑了吧,还是看看Monoid的定义吧:

Monoid由以下条件组成:

1、一个抽象类型A

2、一个二元结合性函数(binary associative function),对传入的两个A类参数进行操作后产生一个A类型结果

3、一个恒等值(identity)

由于Monoid是一个数学类型,它的二元操作函数必须遵循一些定律:

1、结合性(associativity):op(a,op(b,c)) = op(op(a,b),c):这个定律是函数组合(function composition)不可缺的条件

2、二元函数参数中如果有一个是恒等值时操作结果为另一个参数:op(identity,v) = v

我们可以用编程语言来描述Monoid:

1   trait Monoid[A] {                //被封装的类型A
2       def op(a1: A, a2: A): A   //二元函数
3       val zero: A               //恒等值identity
4   }

我们用scala的特质(trait)描述了Monoid。它就是一个抽象的数据类型。

既然Monoid trait是个抽象类型,那么我们可以试着创建几个基础类型的Monoid实例:

 1   val stringConcatMonoid = new Monoid[String] {
 2        def op(s1: String, s2: String) = s1 + s2
 3       val zero = ""   // op(zero,s2) = "" + s2 = s2 恒等值定律
 4   }                                               //> stringConcatMonoid  : ch10.ex1.Monoid[String] = ch10.ex1$$anonfun$main$1$$an
 5                                                   //| on$1@3581c5f3
 6   val intAdditionMonoid = new Monoid[Int] {
 7       def op(i1: Int, i2: Int) = i1 + i2
 8        val zero = 0
 9   }                                               //> intAdditionMonoid  : ch10.ex1.Monoid[Int] = ch10.ex1$$anonfun$main$1$$anon$4
10                                                   //| @340f438e
11   val intMultiplicationMonoid = new  Monoid[Int] {
12       def op(i1: Int, i2: Int) = i1 * i2
13       val zero = 1
14   }                                               //> intMultiplicationMonoid  : ch10.ex1.Monoid[Int] = ch10.ex1$$anonfun$main$1$$
15                                                   //| anon$5@30c7da1e

可以看出,这几个Monoid实例都符合Monoid定律。那我们可以先试着用用。上面提到Monoid最适合一串值的累加操作List[A] => A,我们可以对List[A]进行操作示范:

1  def reduce[A](as: List[A])(m: Monoid[A]): A = {
2     as match {
3         case Nil => m.zero
4         case h::t => m.op(h, reduce(t)(m))
5     }
6   }                                               //> reduce: [A](as: List[A])(m: ch10.ex1.Monoid[A])A

Monoid m是个抽象类型,m.zero和m.op()的具体意义要看Monoid的实例了:

1   reduce(List(1,2,3))(intAdditionMonoid)          //> res3: Int = 6
2   reduce(List("this is ","the string", " monoid"))(stringConcatMonoid)
3                                                   //> res4: String = this is the string monoid

对List[A]的具体累加处理是按照intAdditionMonoid和stringConcatMonoid的二元函数功能进行的。看来Monoid特别适用于List类型的循环操作。可以把reduce函数的参数拓展开来看看:

1   reduce[A](as: List[A])(zero: A)(op: (A,A) => A) : A

这个类型款式跟折叠算法的类型款式非常相似:

1   def foldRight[A,B](as: List[A])(z: B)(f: (A,B) => B): B
2   如果类型B=类型A
3   def foldRight[A](as: List[A])(z: A)(f: (A,A) => A): A

实际上我们可以直接用上面的Monoid实例运算折叠算法:

1   List(1,2,3).foldRight(intAdditionMonoid.zero)(intAdditionMonoid.op)
2                                                   //> res3: Int = 6
3   List("this is ","the string", " monoid").foldLeft(stringConcatMonoid.zero)(stringConcatMonoid.op)
4                                                   //> res4: String = this is the string monoid

左右折叠算法都可以。Monoid的结合性定律(associativity law)可以使List元素运算左右路径相等。

下面我们再试着增加几个Monoid实例:

 1   def optionMonoid[A] = new Monoid[Option[A]] {
 2       def op(o1: Option[A], o2: Option[A]): Option[A] = o1 orElse o2
 3       val zero = None  // op(zero, o1)= None orElse o2 = o2
 4   }                                               //> optionMonoid: [A]=> ch10.ex1.Monoid[Option[A]]{val zero: None.type}
 5   def listConcatMonoid[A] = new Monoid[List[A]] {
 6       def op(l1: List[A], l2: List[A]) = l1 ++ l2
 7       val zero = Nil
 8   }                                               //> listConcatMonoid: [A]=> ch10.ex1.Monoid[List[A]]{val zero: scala.collection.
 9                                                   //| immutable.Nil.type}
10     val booleanOrMonoid = new Monoid[Boolean] {
11         def op(b1: Boolean, b2: Boolean) = b1 || b2
12         val zero = false
13     }                                         //> booleanOrMonoid  : ch10.ex1.Monoid[Boolean] = ch10.ex1$$anonfun$main$1$$anon
14                                                   //| $6@5b464ce8
15     val booleanAndMonoid = new Monoid[Boolean] {
16         def op(b1: Boolean, b2: Boolean) = b1 && b2
17         val zero = true
18     }                                         //> booleanAndMonoid  : ch10.ex1.Monoid[Boolean] = ch10.ex1$$anonfun$main$1$$an
19                                                   //| on$7@57829d67
20     def endoComposeMonoid[A] = new Monoid[A => A] {
21         def op(f: A => A, g: A => A) = f compose g
22         val zero = (a: A) => a    // op(zero, g: A => A) = zero compose g = g
23     }                                         //> endoComposeMonoid: [A]=> ch10.ex1.Monoid[A => A]
24     def endoAndThenMonoid[A] = new Monoid[A => A] {
25         def op(f: A => A, g: A => A) = f andThen g
26         val zero = (a: A) => a   // op(zero, g: A => A) = zero andThen g = g
27     }                                         //> endoAndThenMonoid: [A]=> ch10.ex1.Monoid[A => A]
28     //计算m的镜像Monoid
29     def dual[A](m: Monoid[A]) = new Monoid[A] {
30         def op(x: A, y: A) = m.op(y,x)    //镜像op即时二元参数位置互换
31         val zero = m.zero
32     }                                         //> dual: [A](m: ch10.ex1.Monoid[A])ch10.ex1.Monoid[A]
33     def firstOfDualOptionMonoid[A] = optionMonoid[A]
34                                                   //> firstOfDualOptionMonoid: [A]=> ch10.ex1.Monoid[Option[A]]{val zero: None.ty
35                                                   //| pe}
36     def secondOfDualOptionMonoid[A] = dual(firstOfDualOptionMonoid[A])
37                                                   //> secondOfDualOptionMonoid: [A]=> ch10.ex1.Monoid[Option[A]]

以上几个增加的Monoid实例中endoComposeMonoid和endoAndThenMonoid可能比较陌生。它们是针对函数组合的Monoid。

还是回到对List[A]的累加操作。下面这个函数用Monoid对List[A]元素A进行累加操作:

1   def concatenate[A](l: List[A], m: Monoid[A]): A = {
2       l.foldRight(m.zero){(a,b) => m.op(a,b)}
3   }                                               //> concatenate: [A](l: List[A], m: ch10.ex1.Monoid[A])A
4   concatenate[Int](List(1,2,3),intAdditionMonoid) //> res0: Int = 6

那么如果没有List[A]元素A类型Monoid实例怎么办?我们可以加一个函数:

1 def foldMap[A,B](as: List[A])(m: Monoid[B])(f: A => B): B

如果我们有一个函数可以把A类转成B类 A => B,那我们就可以使用Monoid[B]了:

1   def foldMap[A,B](as: List[A])(m: Monoid[B])(f: A => B): B = {
2     as.foldRight(m.zero)((a,b) => m.op(f(a),b))
3   }

说明一下:foldRight的类型款式:foldRight[A,B](as: List[A])(z: B)(g: (A,B) => B): B。其中(A,B) => B >>> (f(A),B) => B >>> (B,B) => B 就可以使用 Monoid[B].op(B,B)=B了。我们也可以用foldLeft来实现foldMap。实际上我们同样可以用foldMap来实现foldRight和foldLeft: 

1 def foldRight[A,B](la: List[A])(z: B)(f: (A,B) => B): B
2 def foldLeft[A,B](la: List[A])(z: B)(f: (A,B) => B): B
3 def foldMap[A,B](as: List[A])(m: Monoid[B])(f: A => B): B

foldRight和foldLeft的f函数是(A,B) => B,如果用curry表达:A => (B => B),如果能把 A => ? 转成 B => B,那么我们就可以使用endoComposeMonoid[B].op(f: B => B, g: B => B): B。

1   def foldRight[A,B](as: List[A])(z: B)(f: (A,B) => B): B = {
2       foldMap(as)(endoComposeMonoid[B])(a => b => f(a,b))(z)
3   }

说明:foldMap需要f: A => B, foldRight有 (A,B) => B >>> A => B => B >>> f(a)(b) => b >>> f(a,b)(z) >>> f(b)(b)

foldLeft是从左边开始折叠,只需要采用endoComposeMonoid的镜像Monoid把op参数位置调换就行了:

1   def foldLeft[A,B](as: List[A])(z: B)(f: (A,B) => B): B = {
2     foldMap(as)(dual(endoComposeMonoid[B]))(a => b => f(a,b))(z)
3   }

在这节我们简单的介绍了Monoid及它的一些初级类型的实例使用方式。我们也把Monoid代数模型的一面:函数的互通转换及组合稍微示范了一下。在下一节我们将会把Monoid在实际编程中的应用以及Monoid的深度抽象做些讨论。

时间: 2024-10-02 19:47:54

泛函编程(21)-泛函数据类型-Monoid的相关文章

泛函编程(5)-数据结构(Functional Data Structures)

  编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量再对变量进行读写这个过程了.试想想如果没有数据结构,那我们要申明多少个变量呢.所以说,数据结构是任何编程不可缺少的元素.     泛函编程使用泛函数据结构(Functional Data Structure)来支持泛函程序.泛函数据结构的特点是"不可变特性"(Immutability)

泛函编程(23)-泛函数据类型-Monad

  简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可以与任何类型的Monad实例一样在泛函编程中共同提供一套通用的泛函编程方式.所以有人把泛函编程视作Monadic Programming也不为过之.那么,具体什么是Monad呢?     在前面我们讨论过Monoid,我们说过它是一个特殊的范畴(Category),所有数据类型的Monoid实例

泛函编程(24)-泛函数据类型-Monad, monadic programming

   在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个明确的答案吗?我们先从上节设计的Monad组件库中的一些基本函数来加深一点对Monad的了解: 1 trait Monad[M[_]] extends Functor[M] { 2 def unit[A](a: A): M[A] 3 def flatMap[A,B](ma: M[A])(f:

泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Monad实例,就可以使用Monad组件库像for-comprehension这样特殊的.Monad具备的泛函式数据结构内部的按序计算运行流程.针对不同的数据类型,flatMap+unit组件实现方式会有所不同,这是因为flatMap+unit代表着承载数据类型特别的计算行为.之前我们尝试了Li

泛函编程(27)-泛函编程模式-Monad Transformer

  经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative,Traversable,Monad也是我们将来进入实际泛函编程的必需.在前面对这些数据类型的探讨中我们发现: 1.Monoid的主要用途是在进行折叠(Foldable)算法时对可折叠结构内元素进行函数施用(function application). 2.Functor可以对任何高阶数据类型F[

泛函编程(18)-泛函库设计-并行运算组件库

   作为专业的编程人员,我们经常会因为工作需要建立一些工具库.所谓工具库就是针对工作上经常会遇到的一些共性问题预先编制的由一整套函数所组成的函数库.通常这些工具库的功能都是在特别定制的一些数据类型支持下由一系列函数围绕着这些数据类型进行运算而实现的.在泛函编程范畴内也不例外.但在泛函工具库里的函数则更重视函数的组合能力(functional composition):因而泛函的工具库一般称为组件库(combinator library),库内函数则被称之为组件(combinator).组件库的

泛函编程(15)-泛函状态-随意数产生器

  对于OOP程序员来说,泛函状态变迁(functional state transition)是一个陌生的课题.泛函状态变迁是通过泛函状态数据类型(functional state)来实现的.State是一个出现在泛函编程里的类型(type).与其它数据类型一样,State同样需要自身的一套泛函操作函数和组合函数(combinators),我们将在以下章节中讨论有关State数据类型的设计方案.      在正式介绍State类型前,我们先从随意数产生器(RNG: Random Number

泛函编程(9)-异常处理-Option

    Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Option的管子内最多只能存放一个元素,在这个方面Option的数据操作就比List简单的多,因为使用者不必理会数据元素的位置.顺序.Option只有两种状态:包含一个任何类型的元素或者为空.或者这样讲:一个Option实例包含 0 或 1 个元素:None代表为空,Some(x)代表包含一个任

泛函编程(32)-泛函IO:IO Monad

 由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码(impure code)函数中的纯代码部分(pure code)抽离出来形成独立的另一个纯函数.我们通过代码抽离把不纯代码逐步抽离向外推并在程序里形成一个纯代码核心(pure core).这样我们就可以顺利地在这个纯代码核心中实现函数组合.IO Monad就是泛函编程处理副作用代码的一种手段.我们