您的位置:首页 > 其它

翻译 monads-are-elephants 第二部分

2013-09-10 22:40 399 查看


翻译 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(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
[b]
脚注


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

本条目发布于2013 年 6 月 12
日。属于scala分类,被贴了 monadsscala 标签。


TUESDAY, OCTOBER 2, 2007


Monads are Elephants Part 2

In part 1, I introduced Scala's monads
through the parable of the blind men and the elephant. Normally we're supposed to take the view that each blind man doesn't come close to understanding what an elephant is, but I presented the alternative view that if you heard all the blind men describe their
experience then you'd quickly build a surprisingly good understanding of elephants.

In part 2 I'm going to poke at the beast some more by exploring Scala's monad related syntactic sugar: "for comprehensions."


A Little "For"

A very simple "for" looks like this

view plaincopy
to clipboardprint?

val ns = List(1, 2)

val qs = for (n <- ns) yield n * 2

assert (qs == List(2, 4))

The "for" can be read as "for [each] n [in] ns yield n * 2." It looks like a loop, but it's not. This is our old friend map in disguise.

view plaincopy
to clipboardprint?

val qs = ns map {n => n * 2}
The rule here is simple

view plaincopy
to clipboardprint?

for (x <- expr) yield resultExpr
Expands to1

view plaincopy
to clipboardprint?

expr map {x => resultExpr}
And as a reminder, that's equivalent to

view plaincopy
to clipboardprint?

expr flatMap {x => unit(resultExpr)}


More "For"

One expression in a "for" isn't terribly interesting. Let's add some more

view plaincopy
to clipboardprint?

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))

This "for" could be read "for [each] n [in] ns [and each] o [in] os yield n * o. This form of "for" looks a bit like nested loops but it's just a bit of map and flatMap.

view plaincopy
to clipboardprint?

val qs = ns flatMap {n =>

os map {o => n * o }}

It's worth while to spend a moment understanding why this works. Here's how it gets computed (red italics gets turned into bold green):

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

val qs = ns flatMap {n => 
   List(n * 4, n * 5)}

val qs = 
   List(1 * 4, 1 * 5, 2 * 4, 2 * 5)


Now With More Expression

Let's kick it up a notch.

view plaincopy
to clipboardprint?

val qs =

for (n <- ns; o <- os; p <- ps)

yield n * o * p

This "for" gets expanded into

view plaincopy
to clipboardprint?

val qs = ns flatMap {n =>

os flatMap {o =>

{ps map {p => n * o * p}}}}

That looks pretty similar to our previous "for." That's because the rule is recursive

view plaincopy
to clipboardprint?

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

yield resultExpr

expands to

view plaincopy
to clipboardprint?

expr1 flatMap {x1 =>

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

}

This rule gets applied repeatedly until only one expression remains at which point the map form of expansion is used. Here's how the compiler expands the "val qs = for..." statement (again red italics turns to bold green)

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

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

val qs = 
   ns flatMap {n => os flatMap {o => 
   for(p <- ps) yield n * o * p}}

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


An Imperative "For"

"For" also has an imperative version for the cases where you're only calling a function for its side effects. In it you just drop the yield statement.

view plaincopy
to clipboardprint?

val ns = List(1, 2)

val os = List (4, 5)

for (n <- ns; o <- os) println(n * o)

The expansion rule is much like the yield based version but foreach is used instead of flatMap or map.

view plaincopy
to clipboardprint?

ns foreach {n => os foreach {o => println(n * o) }}
Now, you don't have to implement foreach if you don't want to use the imperative form of "for", but foreach is trivial to implement since we already have map.

view plaincopy
to clipboardprint?

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)

()

}

}

In other words, foreach can just call map and throw away the results. That might not be the most runtime efficient way of doing things, though, so Scala allows you to define foreach your own way.


Filtering "For"

So far our monads have built on a few key concepts. These three methods - map, flatMap, and forEach - allow almost all of what "for" can do.

Scala's "for" statement has one more feature: "if" guards. As an example

view plaincopy
to clipboardprint?

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" guards get mapped to a method called filter. Filter takes a predicate function (a function that takes on argument and returns true or false) and creates a new monad without the elements that don't match the predicate. The for statement above gets translated
into something like the following.

view plaincopy
to clipboardprint?

val bNames =

(names filter { bName => bName(0) == 'B' })

.map { bName =>

bName + " is a name starting with B"

}

First the list is filtered for names that start with B. Then that filtered list is mapped using a function that appends " is a name..."

Not all monads can be filtered. Using the container analogy, filtering might remove all elements and some containers can't be empty. For such monads you don't need to create a filter method. Scala won't complain as long as you don't use an "if" guard in
a "for" expression.

I'll have more to say about filter, how to define it in purely monadic terms, and what kinds of monads can't be filtered in the next installment


Conclusion for Part 2

"For" is a handy way to work with monads. Its syntax is particularly useful for working with Lists and other collections. But "for" is more general than that. It expands into map, flatMap, foreach, and filter. Of those, map and flatMap should be defined
for any monad. The foreach method can be defined if you want the monad to be used imperatively and it's trivial to build. Filter can be defined for some monads but not for others.

"m map f" can be implemented as "m flatMap {x => unit(x)}. "m foreach f" can be implemented in terms of map, or in terms of flatMap "m flatMap {x => unit(f(x));()}. Even "m filter p" can be implemented using flatMap (I'll show how next time). flatMap really
is the heart of the beast.

Remember, monads are elephants. The picture I've painted of monads so far emphasizes collections. In part 4, I'll present a monad that isn't a collection and only a container in an abstract way. But before part 4 can happen, part 3 needs to cover some properties
that are true of all monads: the monadic laws.

In the mean time, here's a cheat sheet showing how Haskell's do and Scala's for are related.

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


Footnotes

1. The Scala spec actually specifies that
"for" expands using pattern matching. Basically, the real spec expands the rules I present here to allow patterns on the left side of the <-. It would just muddy this article too much to delve too deeply into the subject.

POSTED BY JAMES IRY AT 9:03
PM 20
COMMENTS
LABELS: FUNCTIONAL PROGRAMMING, MONADS, SCALA
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: