Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern
Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming。这其中透露的Monad重要性则不言而喻。Scalaz是通过Monad typeclass为数据运算的程序提供了一套规范的编程方式,如常见的for-comprehension。而不同类型的Monad实例则会支持不同的程序运算行为,如:Option Monad在运算中如果遇到None值则会中途退出;State Monad会确保状态值会伴随着程序运行流程直到终结;List Monad运算可能会产生多个结果等等。Scalaz提供了很多不同种类的Monad如:StateMonad, IOMonad, ReaderMonad, WriterMonad,MonadTransformer等等,这从另一个角度也重申了Monad概念在泛函编程里的重要性。听起来以上这些描述好像有点摸不着头脑,可能应该把它们放在本篇最终总结,不过我还是想让大家有个大的概念。对下面的讨论细节的理解能有所帮助。我们还是从Monad trait开始介绍吧:
trait Monad[F[_]] extends Applicative[F] with Bind[F] { self =>
//// scalaz/Monad.scala override def map[A,B](fa: F[A])(f: A => B) = bind(fa)(a => point(f(a)))
...
trait Applicative[F[_]] extends Apply[F] { self =>
//// scalaz/Applicative.scala
def point[A](a: => A): F[A]
...
trait Apply[F[_]] extends Functor[F] { self =>
//// scalaz/Apply.scala
def ap[A,B](fa: => F[A])(f: => F[A => B]): F[B]
...
trait Bind[F[_]] extends Apply[F] { self =>
//// scalaz/Bind.scala /** Equivalent to `join(map(fa)(f))`. */
def bind[A, B](fa: F[A])(f: A => F[B]): F[B] override def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B] = {
lazy val fa0 = fa
bind(f)(map(fa0))
}
...
上面这些类型trait的继承关系是这样的:Monad继承了Applicative和Bind,Applicative继承了Apply, Apply继承了Functor, Bind也继承了Apply。所以Monad同时又是Applicative和Functor,因为Monad实现了map和ap函数。一个Monad实例可以调用所有Applicative和Functor提供的组件函数。任何实例只需要实现抽象函数point和bind就可以成为Monad实例,然后就可以使用Monad所有的组件函数了。
Monad所提供的主要注入方法(injected method)是在BindOps和MonadOps里。在BindOps里主要提供了flatMap: scalaz/syntax/BindSyntax.scala
final class BindOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Bind[F]) extends Ops[F[A]] {
////
import Liskov.<~<, Leibniz.=== def flatMap[B](f: A => F[B]) = F.bind(self)(f) def >>=[B](f: A => F[B]) = F.bind(self)(f) def ∗[B](f: A => F[B]) = F.bind(self)(f)
...
主要是这个flatMap函数,在scalaz里用>>=来表示。这是一个大家都起码耳熟的函数:好像flatMap就代表了Monad。在MonadOps里提供的注入方法如下:scalaz/Syntax/MonadSyntax.scala
final class MonadOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Monad[F]) extends Ops[F[A]] {
//// def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self) def whileM[G[_]](p: F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.whileM(p, self) def whileM_(p: F[Boolean]): F[Unit] = F.whileM_(p, self) def untilM[G[_]](p: => F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.untilM(self, p) def untilM_(p: => F[Boolean]): F[Unit] = F.untilM_(self, p) def iterateWhile(p: A => Boolean): F[A] = F.iterateWhile(self)(p) def iterateUntil(p: A => Boolean): F[A] = F.iterateUntil(self)(p) ////
}
看起来这些注入方法都是一些编程语言里的流程控制语法(control flow syntax)。这是不是暗示着Monad最终会实现某种编程语言?我们把这些函数的使用方法放在后面的一些讨论去。我们先来分析一下flatMap函数,因为这是个Monad代表函数。下面是Functor,Applicative和Monad施用函数格式比较:
// Functor : map[A,B] (F[A])(f: A => B): F[B]
// Applicative: ap[A,B] (F[A])(f: F[A => B]): F[B]
// Monad : flatMap[A,B](F[A])(f: A => F[B]): F[B]
以上三种函数款式基本上是一致的。大家都说这就是三种FP的函数施用方式:在一个容器内进行函数的运算后把结果还留在容器内、得到的效果是这样的:F[A] => F[B]。只是它们分别用不同的方式提供这个施用的函数。Functor的map提供了普通函数,Applicative通过容器提供了施用函数ap而Monad则是通过直接函数施用方式来实现F[A] => F[B]: 直接对输入A进行函数施用并产生一个F[B]结果。Monad的这种方式应该不是严格意义上的在容器内进行函数施用。从另一个角度分析,Monad可以被视作某种算法(computation)。Monad F[A]代表了对一个A类型数据的算法(computation)。如果这样说那么Monad就有了全新的解释:Monad就是一种可以对某种类型的数据值进行连续计算的算法(computation):如果我们把flatMap串联起来的话就会是这样的:
// fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))
// for {
// a <- (fa: F[A])
// b <- (fb: F[A])
// c <- (fc: F[A])
// } yield { ... }
class Foo { def bar: Option[Bar] }
class Bar { def baz: Option[Baz] }
class Bar { def baz: Option[Baz] } def compute(maybeFoo: Option[Foo]): Option[Int] =
maybeFoo.flatMap { foo =>
foo.bar.flatMap { bar =>
bar.baz.map { baz =>
baz.compute
}
}
}
def compute2(maybeFoo: Option[Foo]): Option[Int] =
for {
foo <- maybeFoo
bar <- foo.bar
baz <- bar.baz
} yield baz.compute
// ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))
// map[A,B](fa: F[A])(f: A => B): F[B] = fa.flatMap(a => point(f(a)))
从上面的例子好像可以领悟一些关于FP即Monadic Programming的说法。形象的来讲:这个所谓的算法Monad F[]就好像是在F[]这么个壳子里进行传统编程:还记着的话,FP编程既是纯函数(pure function)对F[T]里的T值进行运算,没有中间变量(temp variable),没有副作用(no side-effect)。但现在有了Monad,我们就可以使用传统的行令编程(imperative programming)了。再形象一点来说上面的for loop就像F[]壳子,在for loop内可以进行申明变量,更新状态等OOP式行令编程。但这些变化(mutability)不会漏出for loop之外。不过,本篇所述Monad编程的单一局限性还是很明显的:因为在for loop 内部的操作函数都必须返回同一种类型的Monad实例如:Option[], List[],SomeType[]等等。而且程序运算行为只会受一种类型的特性所控制。如上面所叙,Monad实例的类型控制Monadic程序的运算行为。每一种Monad实例的程序可以有不同的运算方式。如果需要多种类型行为的Monad程序,就需要使用Monad Transformer typeclass了。这个在将来的讨论中自会提及,现在好像说的过头了。我们还是回到Monad的基本操作。
Option是scala标准库的一个类型。它已经是个Monad,所以可以使用flatMap:
.some flatMap {x => (x + ).some } //> res0: Option[Int] = Some(5)
.some >>= { x => (x + ).some } //> res1: Option[Int] = Some(5)
(none: Option[Int]) >>= {x => (x + ).some } //> res2: Option[Int] = None
我们可以用Monad[T] point来把一个普通值A升格到T[A]:
Monad[Option].point() //> res3: Option[Int] = Some(2)
Monad[Option].point() >>= {x => Monad[Option].point(x + )}
//> res4: Option[Int] = Some(5)
(None: Option[Int]) >>= {x => Monad[Option].point(x + )}
//> res5: Option[Int] = None
在上面的例子里我们不断提及Option Monad是有原因的,因为Option类型的Monad典型实例,在控制运算流程时最有特点:可以在中途退出,在遇到None值时可以立即终止运算。
我们用一个比较现实点的例子来示范:我正尝试用自己的方式来练习举重 - 我最多能举起50KG、每个杠铃片重2.5公斤、杠铃两端不必平衡,但一边不得超过另一边多于3个杠铃片(多3个还没问题)。试着用一个自定义类型来模拟举重:
type Discs = Int //杠铃片数量
case class Barbell(left: Discs, right: Discs) {
def loadLeft(n: Discs): Barbell = copy(left = left + n)
def loadRight(n: Discs): Barbell = copy(right = right + n)
}
Barbell(,).loadLeft() //> res8: Exercises.monad.Barbell = Barbell(1,0)
Barbell(,).loadRight() //> res9: Exercises.monad.Barbell = Barbell(1,1)
Barbell(,).loadLeft(-) //> res10: Exercises.monad.Barbell = Barbell(1,1)
现在这个自定义类型Barbell是可以跟踪当前杠铃左右重量状态的。现在我把往杠铃上增加重量片的过程串联起来:
Barbell(,).loadLeft().loadRight().loadRight().loadLeft().loadRight(-)
//> res11: Exercises.monad.Barbell = Barbell(3,3)
可以看到这个过程中有些环节已经超出了我的能力,但杠铃最终状态好像还是合理的。我们需要在重量配置不合理的时候就立即终止。现在我们可以用Option来实现这项功能:
type Discs = Int //杠铃片数量
case class Barbell(left: Discs, right: Discs) {
def loadLeft(n: Discs): Option[Barbell] = copy(left = left + n) match {
case Barbell(left,right) => if ( (left+right <= ) && math.abs(left-right) <= ) Some(Barbell(left,right)) else None
case _ => None
}
def loadRight(n: Discs): Option[Barbell] = copy(right = right + n) match {
case Barbell(left,right) => if ( (left+right <= ) && math.abs(left-right) <= ) Some(Barbell(left,right)) else None
case _ => None
}
}
Barbell(,).loadLeft() //> res8: Option[Exercises.monad.Barbell] = Some(Barbell(1,0))
Barbell(,).loadRight() //> res9: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
Barbell(,).loadLeft(-) //> res10: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
Barbell(,).loadLeft() //> res11: Option[Exercises.monad.Barbell] = None
Barbell(,).loadRight() //> res12: Option[Exercises.monad.Barbell] = None
超出重量平衡的情况返回了None。现在返回值是个Option,而Option是个Monad,所以我们可以用flatMap把每个环节串联起来:
Barbell(,).loadLeft() >>= {_.loadRight()} //> res13: Option[Exercises.monad.Barbell] = Some(Barbell(3,3))
Barbell(,).loadLeft() >>= {_.loadRight() >>= {_.loadRight()}}
//> res14: Option[Exercises.monad.Barbell] = Some(Barbell(3,4))
Barbell(,).loadLeft() >>= {_.loadRight() >>= {_.loadRight() >>= {_.loadLeft()}}}
//> res15: Option[Exercises.monad.Barbell] = Some(Barbell(7,4))
Barbell(,).loadLeft() >>= {_.loadRight() >>= {_.loadLeft()}}
//> res16: Option[Exercises.monad.Barbell] = None
Monad[Option].point(Barbell(,)) >>= {_.loadLeft() >>= {_.loadRight()}}
//> res17: Option[Exercises.monad.Barbell] = Some(Barbell(3,6))
我们的最终目的是用for-comprehension来表述,会更加清晰:
def addWeight: Option[Barbell] = for {
b0 <- Monad[Option].point(Barbell(,))
b1 <- b0.loadLeft()
b2 <- b1.loadRight()
} yield b2 //> addWeight: => Option[Exercises.monad.Barbell]
addWeight //> res18: Option[Exercises.monad.Barbell] = Some(Barbell(3,3)) def addWeight1: Option[Barbell] = for {
b0 <- Monad[Option].point(Barbell(,))
b1 <- b0.loadLeft()
b2 <- b1.loadRight()
} yield b2 //> addWeight1: => Option[Exercises.monad.Barbell]
addWeight1 //> res19: Option[Exercises.monad.Barbell] = None
从以上的例子可以得出:实现了一个数据类型的Monad实例后就可以获取以这个类型控制运算行为的一种简单的编程语言,这种编程语言可以在for loop内部实现传统的行令编程风格。
在本篇讨论中我们介绍了Monad实际上是一种编程模式,并且示范了简单的for loop内部流程运算。在下面的一系列讨论中我们将会了解更多类型的Monad,以及Monad如何能成为功能完善的编程语言。
Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern的更多相关文章
- 24种设计模式--迭代模式【Iterator Pattern】
周五下午,我正在看技术网站,第六感官发觉有人在身后,扭头一看,我 C,老大站在背后,赶忙站起来,“王经理,你找我?” 我说. “哦,在看技术呀.有个事情找你谈一下,你到我办公室来一下.” 老大说. 到 ...
- 24种设计模式--命令模式【Command Pattern】
今天讲命令模式,这个模式从名字上看就很简单,命令嘛,老大发命令,小兵执行就是了,确实是这个意思,但是更深化了,用模式来描述真实世界的命令情况.正在看这本书的你,我猜测分为两类:已经工作的和没有工作的, ...
- 24种设计模式--访问者模式【Visitor Pattern】
今天天气不错,绝对是晴空万里,骄阳似火呀,好,我们今天来讲访问者模式,我们在前面讲了组合模式和迭代器模式,通过组合模式我们能够把一个公司的人员组织机构树搭建起来,给管理带来非常大的便利,通过迭代器模式 ...
- 24种设计模式--原型模式【Prototype Pattern】
今天我们来讲原型模式,这个模式的简单程度是仅次于单例模式和迭代器模式,非常简单,但是要使用好这个模式还有很多注意事项.我们通过一个例子来解释一下什么是原型模式. 现在电子账单越来越流行了,比如你的信用 ...
- 24种设计模式--状态模式【State Pattern】
现在城市发展很快,百万级人口的城市一堆一堆的,那其中有两个东西的发明在城市的发展中起到非常重要的作用:一个是汽车,一个呢是...,猜猜看,是什么?是电梯!汽车让城市可以横向扩展,电梯让城市可以纵向延伸 ...
- 24种设计模式--组合模式【Composite Pattern】
大家在上学的时候应该都学过“数据结构”这门课程吧,还记得其中有一节叫“二叉树”吧,我们上学那会儿这一章节是必考内容,左子树,右子树,什么先序遍历后序遍历什么,重点就是二叉树的的遍历,我还记得当时老师就 ...
- 24种设计模式--建造者模式【Builder Pattern】
在一个周三,快要下班了,老大突然又拉住我,喜滋滋的告诉我“牛叉公司很满意我们做的模型,又签订了一个合同,把奔驰.宝马的车辆模型都交给我们公司制作了,不过这次又额外增加了一个新需求:汽车的启动.停止.喇 ...
- 24种设计模式--门面模式【Facade Pattern】
大家都写过纸质的信件吧,比如给女朋友写情书什么的,写信的过程大家都还记得吧,先写信的内容,然后写信封,把信放到信封中,封好,投递到信箱中进行邮递,这个过程还是比较简单的,虽然简单,这四个步骤都是要跑的 ...
- 24种设计模式--代理模式【Proxy Pattern】
什么是代理模式呢?我很忙,忙的没空理你,那你要找我呢就先找我的代理人吧,那代理人总要知道被代理人能做哪些事情不能做哪些事情吧,那就是两个人具备同一个接口,代理人虽然不能干活,但是被代理的人能干活呀. ...
随机推荐
- HTML按钮实现!!!文件上传,遮罩层
按钮上传文件: <input type="file" /> file的意思为提交 每个游览器的显示方式都不一样哦 遮罩层: 设两个id <style type ...
- Struts2常量的具体用法实例
<!-- 指定Web应用的默认编码集,相当于调用HttpServletRequest的setCharacterEncoding方法 --> <constant name=" ...
- Android Studio 使用技巧
1.导入Android Studio 工程的一些技巧 因为Gradle版本的问题,我们在使用AS导入工程的时候,经常会碰到本地没有该项目的Gradle版本,这时候AS就会去下载这个版本的Gradle, ...
- app的同源和域的问题
app的同源和域的问题
- 【原创】高性能网络编程(二):上一个10年,著名的C10K并发连接问题
1.前言 对于高性能即时通讯技术(或者说互联网编程)比较关注的开发者,对C10K问题(即单机1万个并发连接问题)应该都有所了解."C10K"概念最早由Dan Kegel发布于其个人 ...
- try-catch和throw,throws的区别
java里的异常多种多样,这是一种非常有用的机制,它能帮助我们处理那些我们未知的错误,在java里,关于异常的有throw throws,还有一个try catch 程序块.接下来我们挨个看看这几个的 ...
- Spark MLlib - Decision Tree源码分析
http://spark.apache.org/docs/latest/mllib-decision-tree.html 以决策树作为开始,因为简单,而且也比较容易用到,当前的boosting或ran ...
- MongoDB的学习--聚合
最近要去的新项目使用mysql,趁着还没忘记,总结记录以下MongoDB的聚合. 聚合是泛指各种可以处理批量记录并返回计算结果的操作.MongoDB提供了丰富的聚合操作,用于对数据集执行计算操作.在 ...
- [转载]AxureRP使用参考建议
这些参照建议是马克总结出来的,我只是借用过来给大家参考,在此先感谢一下马克.对于很多学习或者刚使用AxureRP的产品经理们或者朋友们,总会有一些对于AxureRP该怎么使用的更合适想法,也有对Axu ...
- SQL Server逻辑读、预读和物理读
SQL Server数据存储的形式 预读:用估计信息,去硬盘读取数据到缓存.预读100次,也就是估计将要从硬盘中读取了100页数据到缓存. 物理读:查询计划生成好以后,如果缓存缺少所需要的数据,让缓存 ...