翻译 monads-are-elephants 第二部分

原文: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的相关性

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

Leave a Reply

Your email address will not be published. Required fields are marked *