[转]翻译:monads-are-elephants

原文:http://hongjiang.info/monads-are-elephants-part1-chinese/

翻译 monads-are-elephants 第一部分

原文: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

GENERICHASKELLSCALA
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”

 

翻译 monads-are-elephants 第二部分

原文:http://hongjiang.info/monads-are-elephants-part2-chinese/

原文:http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-2.html

在第一部分,我通过盲人摸象的寓言介绍了Scala的monads。正常情况我们认为每个盲人所说的不会让你对大象的理解更接近一些。但我提出另一种观点,如果你听到了所有盲人对他们经历的描述,你很快会对大象建立一个令人惊叹的理解。

在第二部分,我将通过探索scala的monad相关语法糖“for-comprehaension”来更深的戳一下这个怪兽

一个简单的”For”

一个非常简单的”for”表达式,看起来像这样:

val ns = List(1, 2)  
val qs = for (n <- ns) yield n * 2  
assert (qs == List(2, 4))  

“for”可以读作 “for[each] n [in] ns yield n*2″ 它看起来想一个循环,但它不是。这是我们的老朋友map的伪装。

val qs = ns map {n => n * 2}  

这儿的规则很简单

for (x <- expr) yield resultExpr  

展开为(注释1)

expr map {x => resultExpr}  

提醒,它等同于

expr flatMap {x => unit(resultExpr)}  

多一点的”For”

for(的括号)中只有一条表达式并不很有趣,让我们再加一些

val ns = List(1, 2)  
val os = List (4, 5)  
val qs = for (n <- ns; o <- os) yield n * o  
assert (qs == List (1*4, 1*5, 2*4, 2*5))  

这条”for”可以读作”for[each] n [in] ns [and each] o [in] os yield n*o” 这个形式的”for”看起来想一个嵌套循环,但它不过是map和flatMap。

val qs = ns flatMap {n => os map {o => n * o }}  

值得花一点时间理解一下。这儿是它怎么计算的:

val qs = ns flatMap {n => 
            os map { o => n * o } //1
          }

val qs = ns flatMap {n =>        //here
            List(n * 4, n * 5)  //上面的1
         }

val qs = List(1 * 4, 1 * 5, 2 * 4, 2 * 5) //上面的here

更多的表达式

让我们更进一步

val qs = for (n <- ns; o <- os; p <- ps) yield n * o * p  

这个”for”展开为

val qs = ns flatMap {n => 
            os flatMap {o =>   
                {ps map {p => n * o * p}}}}  

这个看起来与之前的那个相似。这是因为转换规则是递归的:

for(x1 <- expr1;...x <- expr) yield resultExpr  

转换为

expr1 flatMap {x1 =>   
    for(...;x <- expr) yield resultExpr  
}  

这条规则可以重复的应用(转换为flatMap),直到只剩下一条表达式,将被转换为map。这儿展示了编译器怎么展开 "val qs = for …"

1. val qs = for (n <- ns;  //第一个表达式
                o <- os;    //第二个
                p <- ps)    //第三个
            yield n * o * p


2. val qs = ns flatMap {n => //上面的第一个表达式,用flatMap翻译
                for(o <- os; 
                p <- ps) 
                yield n * o * p}

3. val qs = ns flatMap { n => 
                os flatMap {o => //第二个也用flatMap翻译
                for(p <- ps) 
                yield n * o * p}}

4. val qs = ns flatMap {n => os flatMap {o =>
                {ps map {p => n * o * p}}} //第三个(最后一个)用map翻译

命令式的”For”

“for”也有一个命令式(imperative)的版本,用于那些你只调用一个函数,并不在意副作用的情况。这种版本只用去掉yield声明。

val ns = List(1, 2)  
val os = List (4, 5)  
for (n <- ns; o <- os)  println(n * o) 

这个展开规则很像yield版本的,但用foreach替代了flatMap或map

ns foreach {n => os foreach {o => println(n * o) }}  

如果你不想使用命令式的”for”,你不需要实现foreach,但既然我们已经实现的map,foreach的实现是微不足道的。

class M[A] {  
    def map[B](f: A=> B) : M[B] = ...  
    def flatMap[B](f: A => M[B]) : M[B] = ...  
    def foreach[B](f: A=> B) : Unit = {  
        map(f)  
        ()  
    }  
}  

换句话说,foreach可以通过调用map并丢掉结果来实现。不过这么做运行时效率可能不好,所以scala允许你用自己的方式定义foreach。

带过滤的 “For”

到目前为止,monads建立在一些关键概念上。有这三个方法: map, flatMap和forEach 几乎所有的for都可实现。

Scala的”for”声明还有一个特征:”if”守护(guard)。看一个例子:

val names = List("Abe", "Beth", "Bob", "Mary")  
val bNames = for (bName <- names;  
                if bName(0) == 'B'  
            ) yield bName + " is a name starting with B"  

assert(bNames == List(
    "Beth is a name starting with B",   
    "Bob is a name starting with B"))  

“if”守护会映射为一个叫做filter的方法。Filter接受一个判定函数(一个返回结果为true或false的函数)并创建一个新的monad过滤出那些与判定不匹配的元素。上面的for语句会翻译成下面的:

val bNames = (names filter { bName => bName(0) == 'B' }) 
             .map { bName =>   
                bName + " is a name starting with B"   
             }  

首先list过滤出名字以’B’开头的,然后对过滤后的list用一个追加一段字符” is a name” 的函数来进行map

不是所有的monads可以被过滤的。用容器来比拟,filtering或许可以删除全部的元素,而有些容器是不能为空的。对于这样的monads你不需要创建filter方法。

只要你不在”for”表达式中使用”if”守护,scala就不会抱怨。关于filter我在下一部分还有更多要说的,如何用纯粹的monadic术语定义来定义,什么样的monads不能被过滤。

第二部份结论

“For”是一个使用monads的简便方式。它的语法在使用List和其它集合时特别有用。但是”for”更通用(比monad),它展开为map,flatMap,foreach和filter。因此,map和flatMap在任何monad都应该定义。如果你想要monad是命令式的,可以定义foreach方法,它的实现也很轻松。filter可以在一些monads中定义,但另一些则不能。

"m map f" 可以实现为 "m flatMap { x => unit(x) }" 。
"m foreach f" 可以根据map来实现,或根据 flatMap 来实现"m flatMap {x => unit(f(x));()}"。甚至 "m filter p" 也可以用 flatMap来实现(我将在下一次展示)。flatMap真是这头野兽的心脏。

记住monads是大象。我所绘画的monads目前都是强调集合特性。在第4部分,我将呈现一个不是集合的monad,只是某种抽象的容器。但在第4部分之前,第3部分需要介绍一些真正monads的属性:monadic法则。同时,这儿有个作弊抄展示 Haskell与Scala的相关性

HASKELLSCALA
do var1<- expn1 

var2 <- expn2 

expn3
for {var1 <- expn1; 

var2 <- expn2; 

result <- expn3 

} yield result
do var1 <- expn1 

var2 <- expn2 

return expn3
for {var1 <- expn1; 

var2 <- expn2; 

} yield expn3
do var1 <- expn1 >> expn2 

return expn3
for {_ <- expn1; 

var1 <- expn2 

} yield expn3
脚注

1) Scala语言规范里实际指定”for”扩展使用模式匹配。实际的规范扩展了我在这儿呈现的规则,允许在左边使用 “<-“来进行模式匹配。太深入钻研的话会使这篇文章模糊混乱(涉及到太多其他概念)。

posted @ 2014-12-10 17:25  Scan.  阅读(218)  评论(0)    收藏  举报