toppic
当前位置: 首页> 科幻小说小说> monoid 在函数式语言的应用

monoid 在函数式语言的应用

2020-10-27 08:19:34

monoid是一种数学理论,他定义了一种特殊的集合操作。

这种特殊的集合操作,满足2个特性,

1. 结合律,

数学有3大定律,结合律,自反律,以及传递律

那么结合律就是 (a + b) + c=  a + (b + c)

也就是结果和操作的组合顺序无关。


再举一个例子,(a + b) + (c + d) = a + (b + c) + d = (a + d) + (b + c) ....

等等操作顺序组合。


这个加法操作就是一个monoid,


那么,在集合论里面, 异或就是一个monoid

(1 xor 0) xor (0 xor 1) = (1 xor 1) xor (0 xor 0)


那么,接下来在计算科学里面,把这种monoid的集合以及数字概念拓展到了函数概念


我们假设,这操作的不是数字和集合,而是函数会怎么样?


这些函数的复合就是复合函数,满足结合律。

也就是 f(g(x)) = g(f(x)), 复合函数我们定义为 f * g = g * f

那么在加几个函数的复合,比如有4个函数,a,b, c, d

那么,在monoid里面就是  (b * c) * (a * d) = (a * b) * (c * d)


那么,在一个函数上面的复合也具备monoid特性,就是 (a * a) * a = a * (a * a)


那么,这个在计算科学上的运用就是,所有具备monoid特性的计算模型,函数,都具备了天生的并行计算的特性。

并行计算是可以大幅度水平计算性能扩展的。


好了,我们看一个scala的例子,就是monoid模式在scala的集合操作里面有很多应用。

有几个方法,一个是foldLeft,FoldRight,Fold

还有一个是Reduce是里面的代表,


这些方法可以运用在Scala的并行计算的持久型数据结构(Persistence Data Structure)上面。


那么,foldLeft 是怎么运用在这些数据结构上面的呢?

在Scala里面要计算一个序列的Sum也就是和,那么就是 

List.foldLeft(0)(_ + _) 或者 List.reduce(_ + _)


为啥说foldLeft,或者reduce可以运用于monoid,因为 

(1 + (2 + (3 + 4) == (1 + 2) + 3 ) + 4)

他们满足结合律,所以不管这些加法计算怎么复合,f(f(f(1,2),3),4)) 的结果和复合的顺序无关,

如何用程序表达这些无限次数的自我函数复合呢?就是reduce和fold,foldLeft,foldRight

我们以FoldLeft为例子,


这个函数接受一个参数,返回的是一个偏序函数(partial function),

接受另一个高阶函数(high order function)作为参数,

这高阶函数运行完,还是一个自我可以复合的高阶函数。



  def foldLeft[B](z: B)(f: (B, A) => B): B 

  

  这个函数的定义很清楚地表达了,他接受2个参数,是一个partial function,

  第二个参数是一个高阶函数,也就是要定义的

  可以自我复合的monoid,就是不管以任何顺序复合本身,函数是一直保持一致的状态结果。

  

  那么其实对于这些monoid函数使用foldright是一样的,从右到左计算和从左到右的计算是一样的结果,

  

monoid的好处是可以运用在并发,并行计算,很多不同的计算单元可以同时计算,和合并。

因为monoid的计算顺序是和结果无关的,那么结果就是不同的计算顺序可以任意结合。


比如上面那么例子 (1 + 2) 和 (3 + 4)是可以同时在2个机器上面进行计算的,那么这个就是并行计算,大大节省了计算时间,

他们的计算结果也可以合并,就是 (3 + 7)这一个计算单元也可以在T2时间点计算。


我们这么来看这个计算顺序,这个是一个树结构的计算过程。


1           T1

+ 2         T2

  + 3       T3

    + 4     T4

    

这个计算是一个树状的计算过程,需要4个时间单元 一共是 O(n) 计算复杂度,在单个计算单位上面


但是如果是并发,并行计算,就是只需要2个计算单元,


1 + 2    3 + 4 T1

   \      / 

      +        T2


那么他们的计算结果其实是一样的,因为monoid是计算结果和计算顺序无关性。


那么接下来,我们说,这个计算时间复杂度是Loga(B) a 是cpu或者计算单元的个数,而B是整个计算单元的分割数目


大家也就看出来了,这个并行计算的计算树,是一个平衡多叉树,可以用这个数据结构去分配和计算所需要的计算时间和复杂度。

平衡计算资源和有效计算时间。



PS 题外话: 适合并发操作的存储式数据结构




在Scala的集合类里面有一个特别适合并行计算的集合库,以par开头的一个包,就是Parallel Collections

里面有list,tree等等数据结构,但是都是基于持久性数据结构的设计,基本是以特殊的并发,

修改版本控制的类似于copy on write的一些数据结构,就是一个linked list,其实在不一样的并发操作,

有多个版本,对于不同的线程,进程。他们的状态是不一样的,最后会合并,基于一些特殊的版本控制计算,和分布式算法

有些是一样的。


这个好比,一个人,同时在多个次元空间,被复制了多个不同的未来,和现在,最后合并成一个。


举个例子,小A今天起床,

线程A也就是一个4维空间,里面在T1 8:00钟时刻,他在看书,

但是在线程B,另一个维度空间里面到了8:00他在刷牙

在线程C另一个维度空间里面8:00他在洗澡,


他在这个时间点上面干啥,我们叫做他的状态,能改变他的状态的就是上帝,也就是计算机。


其实,对于每一个线程里面,他的状态被复制了,这就是持久性数据结构,他的状态在同一个时间点在不同的空间不能互相访问,

但是是复制出来的。


好了,我们举例小B的例子,

小B在线程1里面穿红色衣服,

在线程2里面他要变成蓝色衣服,

在线程3里面他要变成黑色衣服。


那么,这里缺失的是时间,我们说小B是在7点穿红色衣服,还是蓝色衣服,还是黑色衣服,取决于7点是哪个线程修改了他的衣服,

这里就有不一致性,我们无从预测,小B在7点究竟穿什么颜色的衣服。


这个就是可以修改的数据结构,那么他的弱点是,在并发计算,并行计算的时候,不知道究竟小B事穿什么衣服,是红蓝黑,还是黑红蓝,

还是蓝红黑,在最后9点,小B有3个可能的状态,红衣服,蓝衣服,和黑衣服,这取决于,那个线程先运行。


那么,在看小B的例子,

在3个线程的4维空间,每一个小B的状态,被复制了一个小B出来,在线程1,小B永远是红色衣服,在线程2,小B永远是黑色衣服,

在线程3,小B永远是蓝色衣服,那么,到底小B在8:30是什么衣服,我们合并,3个线程空间,小B的状态,

有2种策略,1. 或者小B在最后一个时间点的状态就是他在8:30的衣服颜色,2. 或者由用户自己指定,喜欢看小B穿啥衣服,就是穿啥衣服。


那么,在LinkedList的例子


在 1 -> 2 -> 5 -> 6 的LinkedList里面 线程1 操作是增加节点8 ,线程2操作是在 2 到 5之间插入3


那么

T1: 1 -> 2 -> 5 -> 6 -> 8

T2:      |-> 3 -> 5 -> 6


所以,对于T2这个线程看到的状态永远是 1 -> 2 -> 3 -> 5 -> 6 看不到8

对于 T1 看到的是 1 -> 2 -> 5 -> 6 -> 8 看不到3


那么,我们说,这个数据结构,不覆盖状态,就是所谓的支持并发和并行计算,的无锁计算数据结构,用的就是分布式算法的原理。

是叫做存储式或者持久数据结构。


题外话: idempotent




这里要提到另一个概念,就是idempotent,这个概念的含义是不管如何对于一个函数重复计算,他的结果不会改变。

好了,用一个REST的增删改查的例子,比如,在一个REST api里面执行一个删除操作的时候,网络断线了,被坏人掐断了,

怎么办,那么到底有没有改成,客户端要不要重新操作?这里就涉及到客户端和服务器端的状态不一致,

如果,这个删除是在成功删除后,返回结果时候断线的,那么客户端有数据,服务器端没有,

如果,这个删除是在删除操作前失败,那么,客户端和服务器端还是有数据,

但是返回结果都是一样额额,断线,断线,断线。。。。500 错误。


那客户端到底要不要再删除一次?好纠结,好了,其实不必纠结,直接删除一次好了,

因为 删除操作,是一个具有idempotent特性的操作,就是不管对于服务器施加多少次的删除操作,那么其实最后的结果都是一样的

返回的是对象已经被删除了,或者断线,哪怕断线了,不管是不是成功删除,只要继续对服务器继续一样的操作,得到的结果是一样的。


所以,有些monoid也有idempotent特性,不管如何复合,那么,他们获得的结果都是一样的。

比如XOR等等,这些被叫做Idempotent Monoid









 



友情链接