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 => point(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编程。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/12936.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论