原文:http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html
介绍monads有点像互联网时代的家庭手工业。我想 “为什么要反对传统?”,但这篇文章将以Scala对待monads的方式来描述。
有个古老的寓言,讲述了几个瞎子第一次摸到大象。一个抱着大象的腿说:“它是一棵树”;另一个摸着大象的鼻子则说:“它是一条大蛇”;第三个则说:“它是一把扇子”。。。
从这个寓言我们可以得到个结论:古人相信视觉障碍者喜欢调戏大型哺乳动物(此句英文原意可能带有其它含义)。幸运的是我们生活在一个更开明的时代。我们也应该了解自己的局限,防止它阻碍我们把握事物的全貌,在某些方面,我们都如同盲人一般。
还有一点,与主要意图相反:通过一系列有限的解释,有可能更多的了解大局。如果你从没有看到过一个大象,人们告诉你:“它的腿粗的像树干”,“鼻子像一条蛇”,“尾巴像扫帚”,“耳朵像扇子”,等等,那么你很快就理解了。或许你自己的概念并不完美,但当你真正看到一头大象,它将归入到你脑海中已慢慢建立起来的图片。就像大象要踩到你一样,你会想“哇,它的腿真的像一颗树”。
Monads是容器类型
一个最常用的容器类型是List,我们将花点时间来说它。
我也在之前的文章中提到过Option类型。再提醒一下,Option要么是Some(value)
要么是 None
。
或许List与Option的关系并不那么清晰,这样理解可能有些帮助,你可以把Option看成一个萎缩的List,它只能容纳0或1这两个元素。树(Trees)和集(Sets)也可以当作monads。但要记得monads是一头大象,所以一些monads你不得不眯着点眼睛来把它看做容器。
Monads是参数化的(parameterized,可理解成泛型)。List是个有用的概念,但你需要知道List里面有什么。一个存放字符串的List (List[String]) 与存放整数的List (List[Int]) 是不同的。明显的从一个转换成另一个是有用的。不过这将引入下一个问题。
Monads支持高阶函数
高阶函数是一个将另一个函数作为参数,或返结果为函数的函数。Monads是定义了若干高阶函数的容器。既然我们谈论scala,monads有一系列的高阶”方法”(译注:在scala里函数与方法并不刻意对两个名词划清界限,很多情况下函数就是指方法,关于方法与函数的差异可在这个ppt中了解)。
map就是这样的一个高阶函数。如果你了解函数式语言,那么可能map已经以某种形式被你熟知。map方法接受一个函数,并对容器中的每一个元素应用改函数,返回一个新的容器。举例:
def double(x: Int) = 2 * x
val xs = List(1, 2, 3)
val doubles = xs map double
// or val doubles = xs map {2 * _}
assert(doubles == List(2, 4, 6))
map方法的结果(新产生的容器)不改变Monad的性质(kind),但它可能会改变参数类型…
val one = Some(1)
val oneString = one map {_.toString}
assert(oneString == Some("1"))
这儿 {_.toString}
的意思是对每个元素调用toString
Monads是可组合的(combinable)
现在,我们有一个配置库用来获取参数。对于任何参数,我们取得的都是 Option[String],换句话说我们能否取到一个String取决于这个参数有没有被定义(没有定义得到None)。另外,我们还有个 stringToInt的方法,接受一个String,如果字符串可以解析成Int的话返回Some[Int] ,否则返回None。如果我们尝试用map方法来组合它们,会遇到麻烦:
val opString : Option[String] = config fetchParam "MaxThreads"
def stringToInt(string:String) : Option[Int] = ...
val result = opString map stringToInt
很不幸,我们把Option里面的每个元素执行map操作,并返回另一个Option。变量”result”现在是包含Option元素的Option,即Option[Option[Int]] 类型。这在大多情况下不太有用(我们期望结果是Option[Int])。
激励一个解决方案, 想象如果代替Option,我们使用List ( List[List[Int]]),换句话说一个包含了若干个List的List。假定这样的话我们只需要”flatten”:一个接受一组lists (List[List[A]]) 作为参数,并返回一个把所有结果连接在一起的单一的list(List[A]) 的函数(注释1)。
Option[Option[A]]的flatten函数(与List的相比)有些不同:
def flatten[A](outer:Option[Option[A]]) : Option[A] =
outer match {
case None => None
case Some(inner) => inner
}
如果外部Option为None,结果就是None。否则结果是内部的Option。
这两个flatten
函数有相似的签名:接受一个 M[M[A]]
返回 M[A]
. 但他们实现方式不同。其他的monads也会有它自己flatten
的方式–甚至可能是很复杂的方式。这种可能的复杂也解释了为什么 monads会常常使用“join”替代“flatten”,“join”简洁的表示外部的monad与内部monad的某些方面可能会被组合(combined/joined)。我会继续用”flatten”,因为它符合我们的容器比喻。
现在,Scala不需要你显式的写flatten方法。但对于每个monad,必须要有flatMap方法(注释2)。什么是flatMap? 就像它的字面意思:执行map,然后把结果压扁(flattening)
class M[A] {
private def flatten[B](x:M[M[B]]) : M[B] = ...
def map[B](f: A => B) : M[B] = ...
def flatMap[B](f: A => M[B]) : M[B] = flatten(map(f))
}
有了flatten方法,我们再回到之前有问题的代码处:
val opString : Option[String] = config fetchParam "MaxThreads"
def stringToInt(string:String) : Option[Int] = ...
val result = opString flatMap stringToInt
flatMap方法使我们最终得到的”result”是一个Option[Int]类型。如果我们想要,我们可以对result用一个Int=>Option[Foo]的函数来进行flatMap。然后我们可以用Foo=>Option[Bar]等等之类的函数进行flatMap。
如果深究的话,你会发现很多关于monads的论文使用”bind”一词替代”flatMap”,而Haskell里使用”>>=”操作符;它们都是一个概念。
Monads可以用不同的方式构造
我们看到了怎么使用map来构造一个flatMap方法。还有另一种可能的方式:先实现一个flatMap然后可以基于flatMap实现一个map。为了做到这一点我们需要引入更多概念。在大多monads的论文里称为”unit”的概念,在Haskell里它称为”return”。
Scala是一门面向对象的语言,所以相似的概念可以称为“单个参数构造器”(single argument constructor) 或“工厂”(factory)。基本上,unit接受一个A类型的值,返回一个M[A]
类型的monad。
对于List,unit(x) == List(x)
,对于Option,unit(x) == Some(x)
。(译注,这里的List(x)和Some(x)都是scala的伴生对象的工厂方法)
Scala不需要一个独立的”unit”函数,你写不写unit是一个风格问题。在写这个版本的map时我会显式的写unit方法,只是展示它怎么在内部使用的。
class M[A](value: A) {
private def unit[B] (value : B) = new M(value)
def map[B](f: A => B) : M[B] = flatMap {x => unit(f(x))}
def flatMap[B](f: A => M[B]) : M[B] = ...
}
在这个版本的 flatMap的实现不引用map或flatten,它将一气呵成的完成这两个操作。有趣的是map,它接受一个函数作为参数传入,并把它变成一个适用于flatMap的新函数,新函数看起来像 {x=>unit(f(x))} 意思是先对x执行f函数,然后在对f(x)的结果执行unit。
第一部分的结论
Scala中的monads必须有map和flatMap方法,map可以通过flatMap和一个构造器来实现,或者 flatMap可以通过map和flatten来实现。
flatMap是这头大象的心脏。当你刚开始了解monads,通过map和flatten有助于你构造第一个版本的flatMap。map通常是非常简单直接的。搞清楚flatten对什么有意义是难懂的部分。
当你进入的monads不是集合,你会发现 flatMap 应该先实现,然后map基于flatMap和unit来实现。
在第二部分,我将揭露Scala对monads的语法糖。在第三部分,我将展示大象的DNA:moands法则。最终,在第四部分,我将演示一个monad只能勉强算是一个容器。同时,这儿有个作弊抄比较各种有关monads的论文,Haskell和Scala
Generic | Haskell | Scala |
---|---|---|
M | data M a or newtype M a or instance Monad (M a) |
class M[A] or case class M[A] or trait M[A] |
M a | M a | M[A] |
unit v | return v | new M(v) or M(v) |
map f m | fmap f m | m map f |
bind f m | m >>= f or f =<< m |
m flatMap f |
do | for |
脚注:
1)Scala标准库中的List包含了flatten方法。它非常平滑,但为了解释它我又不得不引入隐式转换,这是个重大干扰(隐式转换是scala里特有的,对monad没有直接的关系)。平滑的部分是flatten对List[List[A]]存在意义,而对 List[A]没有意义,然而Scala里的flatten方法定义在所有的List中,并且是静态的类型检查。
2)我在这儿用了一点简化。Scala不需要特别的方法名称来表示一个monad。你可以取任何方法名如 “germufaBitz” 或 “frizzleMuck”。然而如果你坚持使用map和flatMap的话,你将可以使用Scala的”for comprehensions”