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值:

1 // Functor    :  map[T,U]    (F[T])(f:   T => U):  F[U]
2 // Applicative:  ap[T,U]     (F[T])(f: F[T => U]): F[U]
3 // 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,它的实现方式如下:

1 mplicit object optionFunctor extends Functor[Option] {
2     def map[T,U](ot: Option[T])(f: T => U): Option[U] = ot match {
3         case Some(t) => Some(f(t))
4         case None => None
5     }
6 }

Option Functor实例驱动函数map的意思是说如果目标类型F[T]的值是个Some,那么我们就在Some壳内施用参数提供的一般函数f;如果目标值是None就不施用函数。我们再看看List Functor:

1 implicit object listFunctor extends Functor[List] {
2     def map[T,U](lt: List[T])(f: T => U): List[U] = lt match {
3         case Nil => Nil
4         case head :: tail => f(head) :: map(tail)(f)
5     }
6 }

List Functor的map函数彰显出对一串在壳内元素逐个转变的特性。从List操作方式就很容易理解:list.map(t => transform(t))

我们再看看Option Applicative的实例:

1 implicit object objectApplicative extends Applicative[Option] {
2     def point[T](t: T): Option[T] = Some(t)
3     def ap[T,U](ot: Option[T])(of: Option[T => U]): Option[U] = (ot, of) match {
4         case (Some(t), Some(f)) => Some(f(t))
5         case _ => None
6     }
7 }

Option Applicative的驱动函数ap又一次凸显了Option的特别处理方式:只有在目标值和操作函数都不为None时才施用通过壳提供的操作函数。

再看看Option Monad实例:

1 mplicit object optionMonad extends Monad[Option] {
2     def flatMap[T,U](ot: Option[T])(f: T => Option[U]): Option[U] = ot match {
3         case Some(t) => f(t)
4         case _ => None
5     }
6 }

这个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链表示:

1  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:

1 //   for {
2 //      a <- (fa: F[A])
3 //      b <- (fb: F[A])
4 //      c <- (fc: F[A])
5 //   } 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来实现:

1 ef map[A,B](fa: F[A])(f: A => B): F[B] =
2   fa flatMap {a => point(f(a))}
3 def ap[A,B](fa: F[A])(ff: F[A => B]): F[B] =
4   ff flatMap { f => fa flatMap {a => f(a) }}

值得注意的是:flatMap有着很明显的串性,适合于运算流程管理(workflow)。但实现并行运算就会困难了。这就是Applicative存在的主要原因。如果自定义Monad需要进行并行运算的话就要避免用flatMap实现ap。正确的方式是不用其它的组件函数,直接单独实现ap函数。

很多人自定义Monad可能就是简单希望能用for-comprehension。它是一种简单的FP编程语言(Monadic language):能在一个自定义类型的壳内(context)进行行令编程来实现程序状态转变。如上面强调的那样,我们必须先要搞清楚自定义Monad类型的目的:一开始我们希望能用FP方式实现一些简单的行令编程,如下:

1 var a = 3
2 var b = 4
3 var c = a + b

就是这么简单。不过我们希望用FP方式来实现。那么可不可以这么描述需求:对同样某一种种数据类型的变量进行赋值,然后对这些变量实施操作,在这里是相加操作。那么我们需要一个高阶类型F[T],用F来包嵌一种类型数据T。在壳内运算T后结果还是一个T类型值。

我们先定义一下这个类型吧:

1 trait Bag[A] {
2     def content: A
3 }
4 object Bag {
5     def apply[A](a: A) = new Bag[A] { def content = a }
6 }

形象点解释:一个袋子Bag里装一种可以是任何类型A的东西。

用scalaz来实现Bag类型的Monad实例很简单:

 1 rait Bag[A] {
 2     def content: A
 3 }
 4 object Bag {
 5     def apply[A](a: A) = new Bag[A] { def content = a }
 6     implicit object bagMonad extends Monad[Bag] {
 7         def point[A](a: => A) = Bag(a)
 8         def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = f(ba.content)
 9     }
10 }

只要定义了point,bind函数即可。point能把一个普通类型A的值套入壳子Bag。bind既是flatMap,它决定了从一个运算连接到下一个运算过程中对壳中数据进行的附加处理。可以看到以上bagMonad的bind函数没有附加任何处理,直接对目标壳内数据(ba.content)施用传入函数f。

现在Bag已经是个Monad实例了,我们可以使用所有Monad typeclass提供的函数:

 1 val chainABC = Bag(3) flatMap {a => Bag(4) flatMap {b => Bag(5) flatMap  {c => Bag(a+b+c) }}}
 2                                                   //> chainABC  : Exercises.monad.Bag[Int] = [email protected]
 3 chainABC.content                                  //> res0: Int = 12
 4
 5 val bagABC = Bag(3) >>= {a => Bag(4) >>= {b => Bag(5) map {c => (a+b+c) }}}
 6                                                   //> bagABC  : Exercises.monad.Bag[Int] = [email protected]
 7 bagABC.content                                    //> res1: Int = 12
 8 val bagHello = Bag("Hello") >>= {a => Bag(" John,") >>= {b => Bag("how are you?") map {c => (a+b+c) }}}
 9                                                   //> bagHello  : Exercises.monad.Bag[String] = [email protected]
10                                                   //| 09
11 bagHello.content                                  //> res2: String = Hello John,how are you?

注意我们是如何把壳内变量a,b,c从前面传导到后面的加法操作里的。我们已经实现了Monad的流程式运算。
?现在我们可以使用最希望用的for-comprehension来实现上面的行令编程了:

 1 val addABC: Bag[Int] = for {
 2   a <- Bag(3)
 3   b <- Bag(4)
 4   c <- Bag(5)
 5 } yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = [email protected]
 6 addABC.content                                    //> res2: Int = 12
 7
 8 val concatABC: Bag[String] =
 9 for {
10   a <- Bag("hello")
11   b <- Bag(" jonh,")
12   c <- Bag("how are you ?")
13 } yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = [email protected]
14                                                   //| 772
15 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增加一点影响:

1 trait Bag[+A] {}
2 case class Bagged[+A](content: A) extends Bag[A]
3 case object Emptied extends Bag[Nothing]

我们稍微调整了一下Bag类型。现在Bag由两种状态组成:有东西的袋子Bagged和空袋子Emptied。如果希望我们的Monadic程序在遇到Emptied时能像Option Monad那样立即终止运算并直接返回Emptied结果,我们必须在bind函数里设定这种行为:

 1 trait Bag[+A] {}
 2 case class Bagged[+A](content: A) extends Bag[A]
 3 case object Emptied extends Bag[Nothing]
 4
 5 object Bag {
 6     implicit object bagMonad extends Monad[Bag] {
 7         def point[A](a: => A) = Bagged(a)
 8         def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = ba match {
 9           case Bagged(a) => f(a)
10           case _ => Emptied
11         }
12     }
13 }

在bind函数里我们用模式匹配方式判断输入Bag状态:如果是有装东西的(Bagged)那么像上面的设计一样直接运算f获取下一个Bag状态,如果是空袋子Emptied的话就不做任何运算直接返回Emptied。我们现在可以测试一下上面定义的运算:

 1 val chainABC = Monad[Bag].point(3) flatMap {a => Monad[Bag].point(4) flatMap {b => Monad[Bag].point(5) flatMap  {c => Bagged(a+b+c) }}}
 2                                                   //> chainABC  : Exercises.monad.Bag[Int] = Bagged(12)
 3 val bagABC = Monad[Bag].point(3) >>= {a => Monad[Bag].point(4) >>= {b => Monad[Bag].point(5) map {c => (a+b+c) }}}
 4                                                   //> bagABC  : Exercises.monad.Bag[Int] = Bagged(12)
 5 val bagHello = Monad[Bag].point("Hello") >>= {a => Monad[Bag].point(" John,") >>= {b => Monad[Bag].point("how are you?") map {c => (a+b+c) }}}
 6                                                   //> bagHello  : Exercises.monad.Bag[String] = Bagged(Hello John,how are you?)
 7 val addABC: Bag[Int] = for {
 8   a <- Monad[Bag].point(3)
 9   b <- Monad[Bag].point(4)
10   c <- Monad[Bag].point(5)
11 } yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = Bagged(12)
12
13 val concatABC: Bag[String] =
14 for {
15   a <- Monad[Bag].point("hello")
16   b <- Monad[Bag].point(" jonh,")
17   c <- Monad[Bag].point("how are you ?")
18 } yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = Bagged(hello jonh,how are you ?)
19                                                   //| 

我们可以看到在Bag不是Emptied时,以上这些程序运算行为与上一个版本的Monad程序没有区别。但是如果我们增加了Emptied呢:

1 val bagABC = Monad[Bag].point(3) >>= {a => (Bagged(4): Bag[Int]) >>= {b => Monad[Bag].point(5) >>= { c => (Emptied: Bag[Int]) map {c => (a+b+c) }}}}
2                                                   //> 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情况:

 1 val addABC: Bag[Int] = for {
 2   a <- Monad[Bag].point(3)
 3   x <- (Emptied: Bag[Int])
 4   b <- Monad[Bag].point(4)
 5   c <- Monad[Bag].point(5)
 6 } yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = Emptied
 7
 8 val concatABC: Bag[String] =
 9 for {
10   a <- Monad[Bag].point("hello")
11   x <- (Emptied: Bag[Int])
12   b <- Monad[Bag].point(" jonh,")
13   c <- Monad[Bag].point("how are you ?")
14 } yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = Emptied

不错,正是我们期待的运算行为。

现在我们可以用简单的语言来描述Monad存在的意义:它提供了一套规范的模式来支持FP编程。

时间: 2024-08-04 09:41:00

Scalaz(11)- Monad:你存在的意义的相关文章

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

我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码.像通过键盘显示器进行交流.读写文件.数据库等这些IO操作都会产生副作用.那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段烧CPU的代码,没有任何意义,所以任何类型的程序都必须具备IO功能,而在FP模式中对IO操作有特别的控制方式:具体实现是通过把代码中产生副作用的部分抽离出来延后运算(在所有纯代码运算之后).scalaz的IO Monad就是处理副作用代码延后运算的一种数据结构.我先举个简单的例子来示范如何通过一种数

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

int是java提供的8种原始数据类型之一.Java为每个原始类型提供了封装类,Integer是java为int提供的封装类.int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer.在JSP开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字符串,而int默认的默认值为0,所以用el表达式在文本框中显示时,结果

JAVA面试题基础部分(二)

10.使用 final 关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?使用 final 关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的.例如,对于如下语句:final StringBuffer a=new StringBuffer("immutable");执行如下语句将报告编译期错误:a=new StringBuffer("");但是,执行如下语句则可以通过编译:a.append(" broken!&qu

【Java 面试】面试

一. 笔试题之Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法,虚拟机方面的语法,其他.有些题来自网上搜集整理,有些题来自传智播客学员面试后的反馈,说真的,少数一些网上的面试题,我真怀疑其是否还有存在价值! 1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致. 2.Java有没有goto?

Java 面试-- 1

JAVA面试精选[Java基础第一部分] 这个系列面试题主要目的是帮助你拿轻松到offer,同时还能开个好价钱.只要能够搞明白这个系列的绝大多数题目,在面试过程中,你就能轻轻松松的把面试官给忽悠了.对于那些正打算找工作JAVA软件开发工作的童鞋们来说,当你看到这份题目的时候,你应该感动很幸运,因为,只要你把题目中的内容都搞懂了,在笔试的时候就可以游刃有余,通过面试只有半步之遥了,笔试只能反映你的JAVA技能.不管你是面试各个级别的JAVA工程师.架构师.还是项目经理,这个系列文章都是你最宝贵的资

选一种你比较熟悉的软件,并点评它的优缺点,并描述此类软件的发展历史

windows操作系统, 优点:图形化界面能够更加便捷的对文件进行处理 缺点:容易被黑客破解从而导致信息的丢失 发展历史: 1.Windows是由微软在1983年11月宣布,并在两年后(1985年11月)发行的. 2.Windows版本2.0是在1987.11正式在市场上推出的.该版本对使用者界面做了一些改进.2.0版本还增强了键盘和鼠标界面,特别是加入了功能表和对话框. 3.Windows3.0是在1990年5月22日发布的,它将Win/286和Win/386结合到同一种产品中.Windows

Java面试宝典2013版(超长版)

一. Java基础部分......................................................................................................2 1.一个".java"源文件里能否够包含多个类(不是内部类)?有什么限制?.....2 2.Java有没有goto?........................................................................

了解和熟悉操作系统

了解和熟悉操作系统 一.计算机操作系统的定义和概念: 答:操作系统的定义:操作系统是控制和管理计算机系统的硬件和软件资源.合理地组织工作流程以及方便用户的程序集合. 操作系统的概念:是计算机系统中的一个系统软件,是一些程序模块的集合——它们能以尽量有效.合理的方式组织和管理计算机的软硬件资源,合理的组织计算机的工作流程,控制程序的执行并向用户提供各种服务功能,使得用户能够灵活.方便.有效的使用计算机,使整个计算机系统能高效地运行. 二.操作系统的分类: 答:计算机操作系统总的来说有以下几类:1.

MMU理解

MMU,全称Memory Manage Unit, 中文名——存储器管理单元. 许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序.但随着图形界面的兴起还用用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段.覆盖块0首先运行

java 基础知识总结

1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有一个 public 的类,并且 public 的类名必须与文件名相一致. 2.Java有没有 goto? java中的保留字,现在没有在 java中使用. 3.&和&&的区别. &和&&都可以用作逻辑与的运算符,表示逻辑与(and) ,当运算符两边的表达式的结果都为 true 时,整个运算结果才为 true,否则,只要有一方为 false,