Scalaz(11)- Monad:你存在的意义
前面提到了scalaz是个函数式编程(FP)工具库。它提供了许多新的数据类型、拓展的标准类型及完整的一套typeclass来支持scala语言的函数式编程模式。我们知道:对于任何类型,我们只需要实现这个类型的typeclass实例就可以在对这个类型施用所对应typeclass提供的所有组件函数了(combinator)。突然之间我们的焦点好像都放在了如何获取typeclass实例上了,从而忽略了考虑为什么要使用这些typeclass及使用什么样的typeclass这些问题了。所以可能有人会问我:如何获取Int的Monad实例。我会反问:傻B,你疯了吗(are you insane)?你到底想干什么?这时傻B可能忽然会醒悟还没真正了解自己这样问的目的。看来我们还是回到问题的源头,从使用scalaz的基本目的开始考虑分析了。
我们就围绕scalaz提供的我们都熟悉的typeclass Functor, Applicative, Monad来分析说明吧,因为我们在前面对它们都进行了讨论介绍,为了与scalaz提供的众多其它typeclass有所区分,我们就暂时把它们统称为Monadic typeclass吧。首先,这几个Monadic typeclass不是数据类型,而是代表着某些编程的模式。我们知道FP编程和OOP编程最大的区别就是FP编程的状态不可变性(immutability)、无副作用(no-side-effect)、纯函数组合能力(pure code composability),这就要求FP编程在某种壳子(context)里进行状态转变(transformation)。形象点表达就是F[T]。F[]就是各种独特的壳子(context)而T就是需要运算转变的某种类型值。FP程序的结果形象描述就好像F[T] => F[U]: 代表在F[]壳子内对T进行运算,并把结果U保存在F[]内。既然FP编程相对于OOP编程是种全新的编程方式,那么自然需要一套新的程序状态转变方法,也就是一套新的操作函数施用模式了。Scalaz通过Functor, Applicative, Monad提供了三种基本的函数施用方式,它们都是针对F[T]里的T值:
// Functor : map[T,U] (F[T])(f: T => U): F[U]
// Applicative: ap[T,U] (F[T])(f: F[T => U]): F[U]
// Monad : flatMap[T,U](F[T])(f: T => F[U]): F[U]
以上函数施用方式产生同样的效果:F[T] => F[U],都是典型的FP编程方式。所以可以说Monadic typeclass提供了规范的FP编程框架(template),程序员可以使用这些框架进行FP编程。如果这样解释使用scalaz的目的,是不是更清楚一点了?
从另一个角度解释:scalaz typeclass 代表着抽象编程概念。typeclass是通过即兴多态来实现针对各种类型值的FP式计算的。回到开头傻B的问题:Int是一种基础类型,换句话说就是FP函数施用的目标。Monadic typeclass针对的类型是高阶的F[T]类型。我们需要对在F[]的作用环境里T类型值计算方式进行概括。我们真正需要获取的实例实际上是针对高阶类型F[_]的。所以傻B问了个错误的问题,肯定她当时不知自己在干什么。
现在我们可以分析一下应该使用什么typeclass了。总体来说,我的理解是可以把scalaz typeclass分成种类和特质:
种类定义了FP编程的各种模式。比如Functor, Applicative, Monad都代表不同的编程方式或者说它们都具备不同的程序运算模式。特质是指不同的数据类型所定义的typeclass实例控制着程序的具体运算行为。如Option Monad可以None状态中途终止运算、State Monad确保状态值一直随着程序运算。它们都因为基于不同类型的实例而表现不同的运算行为。Functor, Applicative, Monad的特质则由它们的实例中map, ap, flatMap这三个驱动函数的具体实现方式所决定。我们先看看现成的Option Functor,它的实现方式如下:
mplicit object optionFunctor extends Functor[Option] {
def map[T,U](ot: Option[T])(f: T => U): Option[U] = ot match {
case Some(t) => Some(f(t))
case None => None
}
}
Option Functor实例驱动函数map的意思是说如果目标类型F[T]的值是个Some,那么我们就在Some壳内施用参数提供的一般函数f;如果目标值是None就不施用函数。我们再看看List Functor:
implicit object listFunctor extends Functor[List] {
def map[T,U](lt: List[T])(f: T => U): List[U] = lt match {
case Nil => Nil
case head :: tail => f(head) :: map(tail)(f)
}
}
List Functor的map函数彰显出对一串在壳内元素逐个转变的特性。从List操作方式就很容易理解:list.map(t => transform(t))
我们再看看Option Applicative的实例:
implicit object objectApplicative extends Applicative[Option] {
def point[T](t: T): Option[T] = Some(t)
def ap[T,U](ot: Option[T])(of: Option[T => U]): Option[U] = (ot, of) match {
case (Some(t), Some(f)) => Some(f(t))
case _ => None
}
}
Option Applicative的驱动函数ap又一次凸显了Option的特别处理方式:只有在目标值和操作函数都不为None时才施用通过壳提供的操作函数。
再看看Option Monad实例:
mplicit object optionMonad extends Monad[Option] {
def flatMap[T,U](ot: Option[T])(f: T => Option[U]): Option[U] = ot match {
case Some(t) => f(t)
case _ => None
}
}
这个flatMap函数可以告诉我们更多东西:如果我们把Option[T]视作一个运算的话,那么只要这个运算结果不为None就可以选择连续运算,因为:f: T => Option[U],用文字描述即为给一个T值进行计算后产生另一个运算Option[U],如果再给Option[U]一个值进行计算的话就又会产生另一个运算Opton[V]... 如此持续:
F[A](a => F[B](b => F[C](c => F[D])...))。用flatMap链表示:
fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))
从flatMap串联就比较容易观察到Monad运算的关联依赖性和串联行:后面一个运算需要前面那个运算的结果。而在Option Monad里如果前面的运算产生结果是None的话,串联运算终止并直接返回None作为整串运算的结果。
值得提醒的是连串的flatMap其实也是一种递归算法,但又不属于尾递归,所以拥有和其它FP算法一样的通病:会消耗堆栈,超长的flatMap链条很容易造成堆栈溢出错误(stack overflow)。所以,直接使用Monad编程是不安全的,必须与Trampling数据结构配合使用才行。正确安全的Monad使用方式是通过Trampling结构存放原本在堆栈上的函数调用参数,以heap替换stack来防止stack-overflow。我们会在将来详细讨论Trampling原理机制。
我们可以从上面的flatMap串中推导出for-comprehension:
// for {
// a <- (fa: F[A])
// b <- (fb: F[A])
// c <- (fc: F[A])
// } yield { ... }
从for-comprehension能够更容易看出:我们可以选择在for loop内按要求连续运算F[T]。只要我们能提供a,b,c ...作为运算元素。
按理来说除了Option Monad,其它类型的Monad都具备这种连续运算的可选择性。而Option Monad的特点就在于在运算结果为None时可以立即终止运算。
现在我们可以试着自定义一个类型然后获取个什么实例。不过我们还是要谨记自定义类型的目的何在。我看多数可能是实现Monad实例,这样我们就可以在自定义类型的控制下进行Monadic编程了,即在for-comprehension内进行熟悉的行令编程(imperative programming)。我们应该没什么需要去获取Functor或Applicative实例,而且Monad trait也继承了Functor及Applicative trait,因为map和ap都可以用flatMap来实现:
ef map[A,B](fa: F[A])(f: A => B): F[B] =
fa flatMap {a => point(f(a))}
def ap[A,B](fa: F[A])(ff: F[A => B]): F[B] =
ff flatMap { f => fa flatMap {a => point(f(a)) }}
值得注意的是:flatMap有着很明显的串性,适合于运算流程管理(workflow)。但实现并行运算就会困难了。这就是Applicative存在的主要原因。如果自定义Monad需要进行并行运算的话就要避免用flatMap实现ap。正确的方式是不用其它的组件函数,直接单独实现ap函数。
很多人自定义Monad可能就是简单希望能用for-comprehension。它是一种简单的FP编程语言(Monadic language):能在一个自定义类型的壳内(context)进行行令编程来实现程序状态转变。如上面强调的那样,我们必须先要搞清楚自定义Monad类型的目的:一开始我们希望能用FP方式实现一些简单的行令编程,如下:
var a =
var b =
var c = a + b
就是这么简单。不过我们希望用FP方式来实现。那么可不可以这么描述需求:对同样某一种种数据类型的变量进行赋值,然后对这些变量实施操作,在这里是相加操作。那么我们需要一个高阶类型F[T],用F来包嵌一种类型数据T。在壳内运算T后结果还是一个T类型值。
我们先定义一下这个类型吧:
trait Bag[A] {
def content: A
}
object Bag {
def apply[A](a: A) = new Bag[A] { def content = a }
}
形象点解释:一个袋子Bag里装一种可以是任何类型A的东西。
用scalaz来实现Bag类型的Monad实例很简单:
rait Bag[A] {
def content: A
}
object Bag {
def apply[A](a: A) = new Bag[A] { def content = a }
implicit object bagMonad extends Monad[Bag] {
def point[A](a: => A) = Bag(a)
def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = f(ba.content)
}
}
只要定义了point,bind函数即可。point能把一个普通类型A的值套入壳子Bag。bind既是flatMap,它决定了从一个运算连接到下一个运算过程中对壳中数据进行的附加处理。可以看到以上bagMonad的bind函数没有附加任何处理,直接对目标壳内数据(ba.content)施用传入函数f。
现在Bag已经是个Monad实例了,我们可以使用所有Monad typeclass提供的函数:
val chainABC = Bag() flatMap {a => Bag() flatMap {b => Bag() flatMap {c => Bag(a+b+c) }}}
//> chainABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@c8e4bb0
chainABC.content //> res0: Int = 12 val bagABC = Bag() >>= {a => Bag() >>= {b => Bag() map {c => (a+b+c) }}}
//> bagABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@29626d54
bagABC.content //> res1: Int = 12
val bagHello = Bag("Hello") >>= {a => Bag(" John,") >>= {b => Bag("how are you?") map {c => (a+b+c) }}}
//> bagHello : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@5a63f5
//| 09
bagHello.content //> res2: String = Hello John,how are you?
注意我们是如何把壳内变量a,b,c从前面传导到后面的加法操作里的。我们已经实现了Monad的流程式运算。
现在我们可以使用最希望用的for-comprehension来实现上面的行令编程了:
val addABC: Bag[Int] = for {
a <- Bag()
b <- Bag()
c <- Bag()
} yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@10e41621
addABC.content //> res2: Int = 12 val concatABC: Bag[String] =
for {
a <- Bag("hello")
b <- Bag(" jonh,")
c <- Bag("how are you ?")
} yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@353d0
//| 772
concatABC.content //> res3: String = hello jonh,how are you ?
不要看上面的程序好像很简单,但它代表的意义却是重大的:首先我们实现了FP方式的状态转变:我们虽然使用了行令编程,但最终壳Bag内部的数据content运算结果正是我们编程时所期望的。再就是我们通过flatMap串联持续对多个变量一一进行了赋值,然后用普通的函数把这些变量进行了结合yield (a+b+c)。可以说我们初步尝试实现了FP编程模式(在一个什么壳内进行运算)。
前面说过,for-comprehension可以是一种简单的FP编程语言Monadic language。用它编制的程序运算行为可以受定义它的Monad实例所控制。那么我们就试着为我们的Bag Monad增加一点影响:
trait Bag[+A] {}
case class Bagged[+A](content: A) extends Bag[A]
case object Emptied extends Bag[Nothing]
我们稍微调整了一下Bag类型。现在Bag由两种状态组成:有东西的袋子Bagged和空袋子Emptied。如果希望我们的Monadic程序在遇到Emptied时能像Option Monad那样立即终止运算并直接返回Emptied结果,我们必须在bind函数里设定这种行为:
trait Bag[+A] {}
case class Bagged[+A](content: A) extends Bag[A]
case object Emptied extends Bag[Nothing] object Bag {
implicit object bagMonad extends Monad[Bag] {
def point[A](a: => A) = Bagged(a)
def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = ba match {
case Bagged(a) => f(a)
case _ => Emptied
}
}
}
在bind函数里我们用模式匹配方式判断输入Bag状态:如果是有装东西的(Bagged)那么像上面的设计一样直接运算f获取下一个Bag状态,如果是空袋子Emptied的话就不做任何运算直接返回Emptied。我们现在可以测试一下上面定义的运算:
val chainABC = Monad[Bag].point() flatMap {a => Monad[Bag].point() flatMap {b => Monad[Bag].point() flatMap {c => Bagged(a+b+c) }}}
//> chainABC : Exercises.monad.Bag[Int] = Bagged(12)
val bagABC = Monad[Bag].point() >>= {a => Monad[Bag].point() >>= {b => Monad[Bag].point() map {c => (a+b+c) }}}
//> bagABC : Exercises.monad.Bag[Int] = Bagged(12)
val bagHello = Monad[Bag].point("Hello") >>= {a => Monad[Bag].point(" John,") >>= {b => Monad[Bag].point("how are you?") map {c => (a+b+c) }}}
//> bagHello : Exercises.monad.Bag[String] = Bagged(Hello John,how are you?)
val addABC: Bag[Int] = for {
a <- Monad[Bag].point()
b <- Monad[Bag].point()
c <- Monad[Bag].point()
} yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Bagged(12) val concatABC: Bag[String] =
for {
a <- Monad[Bag].point("hello")
b <- Monad[Bag].point(" jonh,")
c <- Monad[Bag].point("how are you ?")
} yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Bagged(hello jonh,how are you ?)
//|
我们可以看到在Bag不是Emptied时,以上这些程序运算行为与上一个版本的Monad程序没有区别。但是如果我们增加了Emptied呢:
val bagABC = Monad[Bag].point() >>= {a => (Bagged(): Bag[Int]) >>= {b => Monad[Bag].point() >>= { c => (Emptied: Bag[Int]) map {c => (a+b+c) }}}}
//> bagABC : Exercises.monad.Bag[Int] = Emptied
flatMap链条中间出现了Emptied,运算终断,返回Emptied结果。注意下面的表达形式:
Monad[Bag].point(3)
(Bagged(3): Bag[Int])
意思都是一样的。但Bagged(3).flatMap这样写是不行的,因为Bagged(3)不明确是Bag。
再看看在for-comprehension程序中加上Emptied情况:
val addABC: Bag[Int] = for {
a <- Monad[Bag].point()
x <- (Emptied: Bag[Int])
b <- Monad[Bag].point()
c <- Monad[Bag].point()
} yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Emptied val concatABC: Bag[String] =
for {
a <- Monad[Bag].point("hello")
x <- (Emptied: Bag[Int])
b <- Monad[Bag].point(" jonh,")
c <- Monad[Bag].point("how are you ?")
} yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Emptied
不错,正是我们期待的运算行为。
现在我们可以用简单的语言来描述Monad存在的意义:它提供了一套规范的模式来支持FP编程。
Scalaz(11)- Monad:你存在的意义的更多相关文章
- Scalaz(41)- Free :IO Monad-Free特定版本的FP语法
我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码.像通过键盘显示器进行交流.读写文件.数据库等这些IO操作都会产生副作用.那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段 ...
- int和integer;Math.round(11.5)和Math.round(-11.5)
int是java提供的8种原始数据类型之一.Java为每个原始类型提供了封装类,Integer是java为int提供的封装类.int的默认值为0,而Integer的默认值为null,即Integer可 ...
- 深入探访支付宝双11十年路,技术凿穿焦虑与想象极限 | CYZONE特写
小蚂蚁说: 双11十年间,交易规模的指数级增长不断挑战人们的想象力,而对蚂蚁技术团队来说,这不仅是一场消费盛宴,而是无数次濒临压力和焦虑极限的体验,更是技术的练兵场.如今双11对蚂蚁金服而言,已经绝不 ...
- 【Java面试题】59 Math.round(11.5)等於多少? Math.round(-11.5)等於多少?
Math类中提供了三个与取整有关的方法:ceil.floor.round,这些方法的作用与它们的英文名称的含义相对应,例如,ceil的英文意义是天花板,该方法就表示向上取整,Math.ceil(11. ...
- 震撼,java面试题整合(良心制作)11万多字拿去。持续更新【可以收藏】
一.javaSE面试题整合 Java部分 JDK中哪些类是不能继承的?[信雅达面试题] [参考答案] 不能继承的是类是那些用final关键字修饰的类.一般比较基本的类型或防止扩展类无意间破坏原来方法的 ...
- Math.round(11.5)等於多少? Math.round(-11.5)等於多少?
Math类中提供了三个与取整有关的方法:ceil.floor.round,这些方法的作用与它们的英文名称的含义相对应,例如,ceil的英文意义是天花板,该方法就表示向上取整,所以,Math.ceil( ...
- 电信计费业务:预后融合OCS到底应该实扣还是虚扣?
引入OCS的初衷之一是为了让计费系统能够参与到用户的通讯控制中来,也就是所谓的实时信控.用户在没有余额时,通讯就会被停止,不会造成"天价欠费 ",一方面保障用户的利益,一方面也保障 ...
- 【JAVA面试题系列一】面试题总汇--JAVA基础部分
JAVA基础 基础部分的顺序: 基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法 线程的语法,集合的语法,io 的语法,虚拟机方面的语法 每天几道,持续更新!! 1.一个". ...
- MySql常用数据类型分析
整数类型 TINYINT.SMALLINT.MEDIUMINT.INT.BIGINT 分别使用8,16,24,32,64位存储空间,值得范围-2的(N-1)方到2的(N-1)方-1.根据需要存储的范围 ...
- 【转】Java面试宝典2015版(绝对值得收藏超长版)(一)
(转自:http://mp.weixin.qq.com/s?__biz=MjM5MTM0NjQ2MQ==&mid=206619070&idx=1&sn=fcb21001d442 ...
随机推荐
- 批处理集锦——(5)使用dir查找文件
eg dir /a-d /s /b *.bat -d表示不显示.bat结尾的文件夹
- salesforce 零基础学习(二十三)数据记录导出至excel(自定义报表导出)
我们都知道,报表有个功能为导出excel,但是有的时候客户需求往往标准的报表达不到,比如导出excel,其中本月修改的数据字段标红,如下图所示. 这就需要我们去写VF来实现此功能. 需求:将数据表记录 ...
- Asp.Net实现无刷新文件上传并显示进度条(非服务器控件实现)(转)
Asp.Net实现无刷新文件上传并显示进度条(非服务器控件实现) 相信通过Asp.Net的服务器控件上传文件在简单不过了,通过AjaxToolkit控件实现上传进度也不是什么难事,为什么还要自己辛辛苦 ...
- 小实例窥探dotnet垃圾回收
今天项目调试时发现VS中有下面这样的现象. 说明,file.ServerLocation是一个完整的物理路径.第三句代码是错误的,保留只是因为它使用了"s"字符串. 个人认为当 ...
- 国内第一部C#.Net调用Matlab混合编程视频教程
本博客所有文章分类的总目录:[总目录]本博客博文总目录-实时更新 Matlab和C#混合编程文章目录:[目录]Matlab和C#混合编程文章目录 一.视频说明 2014年的5.1,我将这套视频教 ...
- 兼容IE、Firefox的背景半透明内容不透明设置
首先要说明的是背景是内容的祖先元素.如果是兄弟节点那就没有必要记录这篇文章了. 记录一下,知其然也知其所以然. IE8-特点: 1.不支持"opcity:0.5;"这种写法,只支持 ...
- Javascript动画效果(一)
Javascript动画效果(一) 前面我们介绍了Javascript的回到顶部效果,今天呢,我们对Javascript动画做进一步的研究.在这篇博文中我们只介绍简单的匀速运动.简单的缓冲运动和简单的 ...
- 解决The file “FWLifeApp” couldn’t be opened because you don’t have permission to view it.问题
The file “FWLifeApp” couldn’t be opened because you don’t have permission to view it问题是因为项目文件中的Bundl ...
- JSP中<img>标签引用本地图片
问题描述: jsp页面中<img>标签如何读取本地文件夹中的图片. 问题起因: 由于上传图片至本地文件夹中,图片路径为: D:/upload/file/image/img.jpg 所以将这 ...
- MyKTV
namespace MyKTVClient{ public partial class FrmOrderBySinger : Form { private string singerSex = &qu ...