前面提到了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:你存在的意义的更多相关文章

  1. Scalaz(41)- Free :IO Monad-Free特定版本的FP语法

    我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码.像通过键盘显示器进行交流.读写文件.数据库等这些IO操作都会产生副作用.那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段 ...

  2. int和integer;Math.round(11.5)和Math.round(-11.5)

    int是java提供的8种原始数据类型之一.Java为每个原始类型提供了封装类,Integer是java为int提供的封装类.int的默认值为0,而Integer的默认值为null,即Integer可 ...

  3. 深入探访支付宝双11十年路,技术凿穿焦虑与想象极限 | CYZONE特写

    小蚂蚁说: 双11十年间,交易规模的指数级增长不断挑战人们的想象力,而对蚂蚁技术团队来说,这不仅是一场消费盛宴,而是无数次濒临压力和焦虑极限的体验,更是技术的练兵场.如今双11对蚂蚁金服而言,已经绝不 ...

  4. 【Java面试题】59 Math.round(11.5)等於多少? Math.round(-11.5)等於多少?

    Math类中提供了三个与取整有关的方法:ceil.floor.round,这些方法的作用与它们的英文名称的含义相对应,例如,ceil的英文意义是天花板,该方法就表示向上取整,Math.ceil(11. ...

  5. 震撼,java面试题整合(良心制作)11万多字拿去。持续更新【可以收藏】

    一.javaSE面试题整合 Java部分 JDK中哪些类是不能继承的?[信雅达面试题] [参考答案] 不能继承的是类是那些用final关键字修饰的类.一般比较基本的类型或防止扩展类无意间破坏原来方法的 ...

  6. Math.round(11.5)等於多少? Math.round(-11.5)等於多少?

    Math类中提供了三个与取整有关的方法:ceil.floor.round,这些方法的作用与它们的英文名称的含义相对应,例如,ceil的英文意义是天花板,该方法就表示向上取整,所以,Math.ceil( ...

  7. 电信计费业务:预后融合OCS到底应该实扣还是虚扣?

    引入OCS的初衷之一是为了让计费系统能够参与到用户的通讯控制中来,也就是所谓的实时信控.用户在没有余额时,通讯就会被停止,不会造成"天价欠费 ",一方面保障用户的利益,一方面也保障 ...

  8. 【JAVA面试题系列一】面试题总汇--JAVA基础部分

    JAVA基础 基础部分的顺序: 基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法 线程的语法,集合的语法,io 的语法,虚拟机方面的语法 每天几道,持续更新!! 1.一个". ...

  9. MySql常用数据类型分析

    整数类型 TINYINT.SMALLINT.MEDIUMINT.INT.BIGINT 分别使用8,16,24,32,64位存储空间,值得范围-2的(N-1)方到2的(N-1)方-1.根据需要存储的范围 ...

  10. 【转】Java面试宝典2015版(绝对值得收藏超长版)(一)

    (转自:http://mp.weixin.qq.com/s?__biz=MjM5MTM0NjQ2MQ==&mid=206619070&idx=1&sn=fcb21001d442 ...

随机推荐

  1. Permission is only granted to system apps

    原文地址http://jingyan.baidu.com/article/9113f81b2e7a8c2b3314c711.html

  2. 浅谈JAVA集合框架

    浅谈JAVA集合框架 Java提供了数种持有对象的方式,包括语言内置的Array,还有就是utilities中提供的容器类(container classes),又称群集类(collection cl ...

  3. 跨域API

    跨域API 简单跨域请求 只需要简单的设置允许跨域就可以了 def set_default_headers(self): self.set_header('Access-Control-Allow-O ...

  4. hibernate(九) 二级缓存和事务级别详讲

    序言 这算是hibernate的最后一篇文章了,下一系列会讲解Struts2的东西,然后说完Struts2,在到Spring,然后在写一个SSH如何整合的案例.之后就会在去讲SSM,在之后我自己的个人 ...

  5. hdu4750Count The Pairs(最小生成树找瓶颈边)

    /* 题意:就是给你一个图,图的每两个点都有多条路径,每一条路径中都有一条最大边, 所有最大边的最小边(也就是瓶颈边)就是这两点之间的val值!然后给你一个值f, 问有多少个顶点对的val>=f ...

  6. Ext.Net全部Icon图标名称展示

  7. MongoDB的学习--聚合

    最近要去的新项目使用mysql,趁着还没忘记,总结记录以下MongoDB的聚合. 聚合是泛指各种可以处理批量记录并返回计算结果的操作.MongoDB提供了丰富的聚合操作,用于对数据集执行计算操作.在  ...

  8. placeholder的兼容处理(jQuery下)

    这是一个老问题,结合前辈们的经验,需要处理的问题有一下几个. 1.只有输入框(input/textarea)下的palaceholder属性存在的时候才需要处理这类兼容 2.处理好输入框上焦点和是焦点 ...

  9. elasticsearch 查询(match和term)

    elasticsearch 查询(match和term) es中的查询请求有两种方式,一种是简易版的查询,另外一种是使用JSON完整的请求体,叫做结构化查询(DSL). 由于DSL查询更为直观也更为简 ...

  10. 近期总结:generator-web,前端自动化构建的解决方案

    本文结合最近的工作经验,总结出一个较简洁的前端自动化构建方案,主张css和js的模块化,并通过grunt的自动化构建,有效地解决css合并,js合并和图片优化等问题,对于提高前端性能和项目代码质量有一 ...