翻译 monads-are-elephants 第一部分

来源:互联网 发布:淘宝店铺自助服务在哪 编辑:程序博客网 时间:2024/06/16 12:12

翻译 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

GENERICHASKELLSCALAMdata M a

or

newtype M a

or

instance Monad (M a)class M[A] 

or

case class M[A] 

or

trait M[A]M aM aM[A]unit vreturn vnew M(v) 

or

M(v)map f mfmap f mm map fbind f mm >>= f 

or

f =<< mm flatMap f dofor
脚注:

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”

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


TUESDAY, SEPTEMBER 18, 2007

Monads are Elephants Part 1

Introductions to monads are bit of cottage industry on the Internet. So I figured, "why buck tradition?" But this article will present Scala's way of dealing with monads.

An ancient parable goes that several blind men were experiencing their first elephant. "It's a tree," one said while wrapping his arms around its legs. "A large snake," another said while holding its trunk. A third said...um, something about a broom or a fan or whatever.

From this parable we can conclude this: the ancients believed that the visually impaired like to fondle large mammals. Fortunately we live in a more enlightened age.

We're also supposed to learn something about how our limitations can prevent us from grasping the whole picture and that we're all blind in some way. It's so Zen.

I think there's a third lesson to be learned - the opposite of the main intent: that it's possible to learn much about the big picture by getting a series of limited explanations. If you had never seen an elephant and people told you things like "it has legs as thick as tree trunks," "a nose like a snake," "a tail like a broom," "ears like fans," etc. then you'd soon have a pretty good understanding. Your conception wouldn't be perfect, but when you finally saw an elephant it would fit neatly into the mental picture you had slowly built up. Just as the elephant was about to step on you, you'd think "wow, the legs really are like trees."

Monads are Container Types

One of the most commonly used container types is List and we'll spend some time with it. I also mentioned Option in a previous article. As a reminder, an Option is always either Some(value) or None. It might not be clear how List and Option are related, but if you consider an Option as a stunted List that can only have 0 or 1 elements, it helps. Trees and Sets can also be monads. But remember that monads are elephants, so with some monads you may have to squint a bit to see them as containers.

Monads are parameterized. List is a useful concept, but you need to know what's in the List. A List of Strings (List[String]) is pretty different from a List of Ints (List[Int]). Obviously it can be useful to convert from one to the other. Which leads us to the next point.

Monads Support Higher Order Functions

A higher order function is a function that takes a function as a parameter or returns a function as a result. Monads are containers which have several higher order functions defined. Or, since we're talking about Scala, monads have several higher order methods.

One such method is map. If you know any functional languages then you're probably familiar with map in one form or another. The map method takes a function and applies it to each element in the container to return a new container. For instance

view plaincopy to clipboardprint?
  1. def double(x: Int) = 2 * x  
  2. val xs = List(123)  
  3. val doubles = xs map double  
  4. // or val doubles = xs map {2 * _}  
  5. assert(doubles == List(246))  

Map does not change the kind of monad, but may change its parameterized type...

view plaincopy to clipboardprint?
  1. val one = Some(1)  
  2. val oneString = one map {_.toString}  
  3. assert(oneString == Some("1"))  

Here the {_.toString} notation means that the toString method should be called on the element.

Monads are Combinable

Now let's say we have a configuration library that let's us fetch parameters. For any parameter we'll get back an Option[String] - in other words we may or may not get a string depending on whether the parameter is defined. Let's say we also have a function, stringToInt, which takes a String and returns Some[Int] if the string is parseable as an integer or None if it's not. If we try to combine them using map we run into trouble.

view plaincopy to clipboardprint?
  1. val opString : Option[String] = config fetchParam "MaxThreads"  
  2. def stringToInt(string:String) : Option[Int] = ...  
  3. val result = opString map stringToInt  

Unfortunately, since we started with an Option and mapped its contained element with a function that results in another Option, the variable "result" is now an Option that contains an Option, ie result is an Option[Option[Int]]. That's probably not terribly useful in most cases.

To motivate a solution, imagine if instead of Option we'd used List and ended up with List[List[Int]]] - in other words a list containing some number of lists. Given that, we just need "flatten" - a function which takes a list of lists (List[List[A]]) and returns a single list (List[A]) by concatenating everything together.1

A flatten function for Option[Option[A]] works a bit differently.

view plaincopy to clipboardprint?
  1. def flatten[A](outer:Option[Option[A]]) : Option[A] =   
  2.    outer match {  
  3.      case None => None   
  4.      case Some(inner) => inner   
  5.    }  

If the outer option is None, then result is None. Otherwise the result is the inner Option.

These two flatten functions have similar signatures: they take an M[M[A]] and turn it into an M[A]. But the way they do it is quite different. Other monads would have their own ways of doing flatten - possibly quite sophisticated ways. This possible sophistication is why explanations of monads will often use "join" instead of "flatten." "Join" neatly indicates that some aspect of the outer monad may be combined (joined) with some aspect of the inner monad. I'll stick with "flatten," though, because it fits with our container analogy.

Now, Scala does not require you to write flatten explicitly. But it does require that each monad have a method called flatMap.2. What's flatMap? It's exactly what it sounds like: doing a map and then flattening the result.

view plaincopy to clipboardprint?
  1. class M[A] {  
  2.   private def flatten[B](x:M[M[B]]) : M[B] = ...  
  3.   def map[B](f: A => B) : M[B] = ...  
  4.   def flatMap[B](f: A => M[B]) : M[B] = flatten(map(f))  
  5. }  

With that, we can revisit our problematic code...

view plaincopy to clipboardprint?
  1. val opString : Option[String] = config fetchParam "MaxThreads"  
  2. def stringToInt(string:String) : Option[Int] = ...  
  3. val result = opString flatMap stringToInt  

Because of flatMap we end up with "result" being an Option[Int]. If we wanted, we could take result and flatMap it with a function from Int to Option[Foo]. And then we could faltMap that with a function from Foo to Option[Bar], etc.

If you're keeping score, many papers on monads use the word "bind" instead of "flatMap" and Haskell uses the ">>=" operator. It's all the same concept.

Monads Can Be Built In Different Ways

So we've seen how the flatMap method can be built using map. It's possible to go the other way: start with flatMap and create map based on it. In order to do so we need one more concept. In most papers on monads the concept is called "unit," in Haskell it's called "return." Scala is an object oriented language so the same concept might be called a single argument "constructor" or "factory." Basically, unit takes one value of type A and turns it into a monad of type M[A]. For List, unit(x) == List(x) and for Option, unit(x) == Some(x).

Scala does not require a separate "unit" function or method, and whether you write it or not is a matter of taste. In writing this version of map I'll explicitly write "unit" just to show how it fits into things.

view plaincopy to clipboardprint?
  1. class M[A](value: A) {  
  2.   private def unit[B] (value : B) = new M(value)  
  3.   def map[B](f: A => B) : M[B] = flatMap {x => unit(f(x))}  
  4.   def flatMap[B](f: A => M[B]) : M[B] = ...  
  5. }  

In this version flatMap has to be built without reference to map or flatten - it will have to do both in one go. The interesting bit is map. It takes the function passed in (f) and turns it into a new function that is appropriate for flatMap. The new function looks like {x => unit(f(x))} meaning that first f is applied to x, then unit is applied to the result.

Conclusion for Part I

Scala monads must have map and flatMap methods. Map can be implemented via flatMap and a constructor or flatMap can be implemented via map and flatten.

flatMap is the heart of our elephantine beast. When you're new to monads, it may help to build at least the first version of a flatMap in terms of map and flatten. Map is usually pretty straight forward. Figuring out what makes sense for flatten is the hard part.

As you move into monads that aren't collections you may find that flatMap should be implemented first and map should be implemented based on it and unit.

In part 2 I'll cover Scala's syntactic sugar for monads. In part 3 I'll present the elephant's DNA: the monad laws. Finally, in part 4 I'll show a monad that's only barely a container. In the meantime, here's a cheat sheet for translating between computer science papers on monads, Haskell, and Scala.

GenericHaskellScalaMdata M a
or
newtype M a
or
instance Monad (M a)class M[A]
or
case class M[A]
or
trait M[A]M aM aM[A]unit vreturn vnew M(v)
or
M(v)map f mfmap f mm map fbind f mm >>= f
or
f =<< mm flatMap fjoinjoinflatten dofor

Footnotes

1. The Scala standard library includes a flatten method on List. It's pretty slick, but to explain it I would have to go into implicit conversions which would be a significant distraction. The slick part is that flatten makes sense on List[List[A]] but not on List[A], yet Scala's flatten method is defined on all Lists while still being statically type checked.

2. I'm using a bit of shorthand here. Scala doesn't "require" any particular method names to make a monad. You can call your methods "germufaBitz" or "frizzleMuck". However, if you stick with map and flatMap then you'll be able to use Scala's "for comprehensions"

Newer PostOlder PostHome

原创粉丝点击