泛函编程(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: A => M[B]): M[B]
 4       def map[A,B](ma: M[A])(f: A => B): M[B] = {
 5           flatMap(ma){a => unit(f(a))}
 6       }
 7       def map2[A,B,C](ma: M[A], mb: M[B])(f: (A,B) => C): M[C] = {
 8           flatMap(ma) { a => map(mb){ b => f(a,b) }}
 9       }
10       def sequence[A](lm: List[M[A]]): M[List[A]] = {
11           lm.foldRight(unit(Nil: List[A])){(a,b) => map2(a,b){_ :: _} }
12       }
13       //递归方式sequence
14       def sequence_r[A](lm: List[M[A]]): M[List[A]] = {
15           lm match {
16               case Nil => unit(Nil: List[A])
17               case h::t => map2(h,sequence_r(t)){_ :: _}
18           }
19       }
20       //高效点的sequence(可以并行运算Par)
21       def bsequence[A](iseq: IndexedSeq[M[A]]): M[IndexedSeq[A]] = {
22           if (iseq.isEmpty) unit(Vector())
23           else if (iseq.length == 1) map(iseq.head){Vector(_)}
24           else {
25               val (l,r) = iseq.splitAt(iseq.length / 2)
26               map2(bsequence(l),bsequence(r)) {_ ++ _}
27           }
28       }
29       def traverse[A,B](la: List[A])(f: A => M[B]): M[List[B]] = {
30           la.foldRight(unit(Nil: List[B])){(a,b) => map2(f(a),b){_ :: _}}
31       }
32       def replicateM[A](n: Int, ma: M[A]): M[List[A]] = {
33           if (n == 0) unit(Nil)
34           else map2(ma,replicateM(n-1,ma)) {_ :: _}
35       }
36       def factor[A,B](ma: M[A], mb: M[B]): M[(A,B)] = {
37           map2(ma,mb){(a,b) => (a,b)}
38       }
39       def cofactor[A,B](e: Either[M[A],M[B]]): M[Either[A,B]] = {
40           e match {
41               case Right(b) => map(b){x => Right(x)}
42               case Left(a) => map(a){x => Left(x)}
43           }
44       }
45   }

我们分别用M[A]对应List[A],Option[A]及Par[A]来分析一下sequence函数的作用:

1. sequence >>> 用map2实现 >>> 用flatMap实现:

对于List: sequence[A](lm: List[M[A]]): M[List[A]] >>> sequence[A](lm: List[List[A]]): List[List[A]]

>>> map2(list(list1),list(list2)){_ :: _} ,把封装在list里的list进行元素分拆交叉组合,

例:(List(List(1,2),List(3,4)) >>> List[List[Int]] = List(List(1, 3), List(1, 4), List(2, 3), List(2, 4))

sequence的作用体现在List.map2功能。而List.map2则是由List.flatMap实现的。所以sequence的行为还是依赖于List实例中flatMap的实 现方法

对于Option: sequence[A](lm: List[M[A]]): M[List[A]] >>> sequence[A](lm: List[Option[A]]): List[Option[A]]

>>> map2(list(opton1),list(option2)){_ :: _} ,把封装在list里的元素option值串成list,

例:(List(Some(1),Some(2),Some(3)) >>> Option[List[Int]] = Some(List(1, 2, 3))

由于sequence的行为还是依赖于实例中flatMap的实现,Option 的特点:flatMap None = None 会产生如下效果:

List(Some(1),None,Some(3)) >>> Option[List[Int]] = None

对于Par: sequence[A](lm: List[M[A]]): M[M[A]] >>> sequence[A](lm: List[Par[A]]): List[Par[A]]

>>> map2(list(par1),list(par2)){_ :: _} ,运行封装在list里的并行运算并把结果串成list,

这里Par.flatMap的功能是运行par,run(par)。这项功能恰恰是并行运算Par的核心行为。

从分析sequence不同的行为可以看出,Monad的确是一个通用概括的抽象模型。它就是一个很多数据类型组件库的软件接口:使用统一的函数名称来实现不同数据类型的不同功能效果。

与前面讨论过的Monoid一样,Monad同样需要遵循一定的法则来规范作用、实现函数组合(composition)。Monad同样需要遵循结合性操作(associativity)及恒等(identity)。

Monoid的结合性操作是这样的:op(a,op(b,c)) == op(op(a,b),c)  对Monad来说,用flatMap和map来表达结合性操作比较困难。但我们如果不从Monadic值M[A](Monadic value)而是循Monadic函数A=>M[B](Monadic function)来证明Monad结合性操作就容易多了。

A=>[B]是瑞士数学家Heinrich Kleisli法则的箭头(Kleisli Arrow)。我们可以用Kleisli Arrow来实现一个函数compose:

def compose[A,B,C](f: A=>[B], g: B=>M[C]): A=>M[C]。从函数款式看compose是一个Monadic函数组合。我们从返回值的类型A=>M[C]得出实现框架 a => ???;从传入参数类型 B=>M[C]可以估计是flatMap(M[A])(B=>M[C]; 所以:

1       def compose[A,B,C](f: A => M[B], g: B => M[C]): A => M[C] = {
2         a => flatMap(f(a))(g)
3       }

注意:compose的实现还是通过了flatMap这个主导Monad实例行为的函数。有了compose我们就可以证明:

compose(f,compose(g,h)) == compose(compose(f,g),h)

flatMap和compose是互通的,可以相互转换。我们可以用compose来实现flatMap:

1       def flatMapByCompose[A,B](ma: M[A])(f: A => M[B]): M[B] = {
2           compose((_ : Unit) => ma, f)(())
3       }

我们可以用例子来证明它们的互通性:

1  optionMonad.flatMap(Some(12)){a => Some(a + 10)}//> res12: Option[Int] = Some(22)
2  optionMonad.compose((_: Unit) => Some(12), { (a : Int) => Some(a + 10)}) (())
3                                                   //> res13: Option[Int] = Some(22)

至于Monad恒等性,我们已经得到了unit这个Monad恒等值:

def unit[A](a: A): M[A]。通过unit我们可以证明Monad的左右恒等:

compose(f,unit) == f

compose(unit,f) == f

由于compose是通过flatMap实现的。compose + unit也可以成为Monad最基本组件。实际上还有一组基本组件join + map + unit:

1      def join[A](mma: M[M[A]]): M[A] = flatMap(mma) {ma => ma}

又是通过flatMap来实现的。

我们同样可以用join来实现flatMap和compose:

1       def flatMapByJoin[A,B](ma: M[A])(f: A => M[B]): M[B] = {
2           join(map(ma)(f))
3       }
4       def composeByjoin[A,B,C](f: A => M[B], g: B => M[C]): A => M[C] = {
5           a => join(map(f(a))(g))
6       }

仔细观察函数款式(signature),推导并不难。map A=>M[B] >>> M[M[B]],实际上join是个展平函数M[M[A]] >>> M[A]。

虽然有三种基本组件,我还是比较倾向于flatMap,因为只要能flatMap就是Monad。对我来说Monadic programming就是flatMap programming,其中最重要的原因是scala的for-comprehension。for-comprehension是scala的特点,只要是Monad实例就可以用for-comprehension,也可以说只要能flatMap就可以吃到for-comprehension这块语法糖。我们用一个比较复杂但实用的数据类型来说明:

在前面我们曾经实现了State类型。而且我们也实现了State类型的map, flatMap这两个函数:

 1 case class State[S, A](run: S => (A, S)) {
 2   def map[B](f: A => B): State[S, B] =
 3     State(s => {
 4       val (a, s1) = run(s)
 5       (f(a), s1)
 6     })
 7   def flatMap[B](f: A => State[S, B]): State[S, B] =
 8     State(s => {
 9       val (a, s1) = run(s)
10       f(a).run(s1)
11 }) }

既然实现了flatMap, 那么State就可以是Monad的了吧。我们试着建一个State Monad实例:

State类定义是这样的:case class State[S,+A](run: S => (A, S))

val StateMonad = new Monad[State[???, 糟糕,Monad[M[_]],M是个接受一个类参数的高阶类型,而State[S,A]是个接受两个类参数的高阶类型,该怎么办呢?我们可以这样解释State:State[S,_]:实际上State[S,_]是一组不同S的State[A],换句话说:State不只有一个Monad实例而是一类的Monad实例。我们可以这样表述这类的Monad:

1   class StateMonad[S] {
2       type StateS[A] = State[S,A]
3       val monad = new Monad[StateS] {
4         def unit[A](a: A): StateS[A] = State(s => (a,s))
5           def flatMap[A,B](ma: StateS[A])(f: A => StateS[B]): StateS[B] = flatMap(ma)(f)
6       }
7   }

我们可以这样使用以上的State Monad:StateMonad[List[Int]].monad

在上面我们遇到的问题是由于State类型与Monad M[_]类型不兼容引起的。这个问题会被scala编译器的类系统(type system)逮住,然后终止编译过程。是不是能从解决类系统问题方面着手呢?我们可以用type lambda来糊弄一下类系统:

1   def StateMonad[S] = new Monad[({type StateS[A]=State[S,A]})#StateS] {
2     def unit[A](a: A) = State(s => (a,s))
3     def flatMap[A,B](sa: State[S,A])(f: A => State[S,B]): State[S,B] = flatMap(sa)(f)
4   }

看,在Monad类参数里糊弄了类系统后,StateMonad内部沿用了State正常表述,没任何变化。type lambda在scalaz里使用很普遍,主要还是解决了数据类型参数不匹配问题。

实现了State Monad后我们可以看个相关例子:

 1   val intStateMonad = StateMonad[Int]             //> intStateMonad  : ch6.state.Monad[[A]ch6.state.State[Int,A]] = ch6.state$$an
 2                                                   //| [email protected]
 3   def zipWithIndex[A](as: List[A]): List[(Int,A)] = {
 4       as.foldLeft(intStateMonad.unit(List[(Int, A)]()))((acc,a) => for {
 5           n <- getState
 6           xs <- acc
 7           _ <- setState(n+1)
 8       } yield((n,a) :: xs)).run(0)._1.reverse
 9   }                                               //> zipWithIndex: [A](as: List[A])List[(Int, A)]
10
11   val lines=List("the quick","fox is","running","and runnng","...")
12                                                   //> lines  : List[String] = List(the quick, fox is, running, and runnng, ...)
13   zipWithIndex(lines)                             //> res3: List[(Int, String)] = List((0,the quick), (1,fox is), (2,running), (3
14                                                   //| ,and runnng), (4,...))

说明:foldLeft(z:B)(f:(B,A)=>B)的z是个intStateMonad实例类型B,所以foldLeft的操作函数就是:(intStateMonad,A)=>intStateMonad,我们可以使用for-comprehension。这个操作函数的返回结果是个intStateMonad实例;所以我们可以用State类的run(0)来运算State转换;State的状态起始值是0。

以上的例子做了些什么:它把List[String]转成了List[(Int,String)],把List[String]中每一个字串进行了索引。在这个例子里我们了解了Monad的意义:

1、可以使用for-comprehension

2、支持泛函式的循序命令执行流程,即:在高阶类结构内部执行操作流程。flatMap在这里起了关键作用,它确保了流程环节间一个环节的输出值成为另一个环境的输入值

那么我们可不可以说:Monad就是泛函编程中支持泛函方式流程式命令执行的特别编程模式。

时间: 2024-10-23 12:52:49

泛函编程(24)-泛函数据类型-Monad, monadic programming的相关文章

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

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

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

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

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

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

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

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

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

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

泛函编程(34)-泛函变量:处理状态转变-ST Monad

泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Referentially Transparent(RT,等量可替换的),它的意思是:在一段程序p中,所有的表达式e都可以用e的运算结果替代而不影响到p的运算结果,那么e就是RT等量可替换的,也就是说程序p是由纯代码组成的.但如果程序p中包含了一些变量,这些变量的状态就会影响到程序中e的运算结果,那么p

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

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

泛函编程(30)-泛函IO:Free Monad-Monad生产线

在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法运行时在堆栈上寄存程序状态,用Trampoline进行递归算法时程序状态是保存在Trampoline的数据结构里的.数据结构是在heap上的,所以可以实现以heap换stack的效果.这种以数据结构代替函数调用来解决问题的方式又为泛函编程提供了更广阔的发展空间. 我们知道,任何涉及IO的运算都会面临

泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow

泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法.比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无法被称为Monadic Programming了.虽然递归算法能使代码更简洁易明,但同时又以占用堆栈(stack)方式运作.堆栈是软件程序有限资源,所以在使用递归算法对大型数据源进行运算时系统往往会出现StackOverflow错误.如果不想办法解决递归算法带来的StackOverflow问题,泛函