您的位置:首页 > 其它

Monads in Scala Part One: Maybe[Person]

2013-09-10 21:44 435 查看



Monads in Scala Part One: Maybe[Person]

Intro

I've split the introduction to this post into two parts. The first part is long and sentimental, describing my personal journey with monads up to now. The second part is short and to the point. Feel free to skip over the next section if you're not interested
in the sappy stuff - you won't be missing anything important.

Sappy Intro

I've been programming Scala for a little over two years now, and I love it. I was an old-time Java hack, firmly embedded in the OO programming paradigm. Scala to me was largely like a souped up Java, with all the clunky, broken things about Java fixed (aside
from type erasure). I had taken courses Scheme and the lambda calculus in college, and I thought I had a pretty good understanding of functional programming. However, while programming Scala, and getting exposure to the language, libraries, and community,
I began to discover that there is a lot more to functional programming than I originally thought. By far the most prominent example of this was the the importance of the concept of the monad in the Scala community.

I didn't know what a monad was, and over the past couple of years, I've made various efforts to try to understand it. For the most part, the videos I watched, and the articles I read, left me with one of two impressions: Either it didn't make any sense, or
I just couldn't see what the big deal was. As I continued to explore, I began to notice that most of the explanatory examples are in Haskell, and not understanding the Haskell language or syntax was a serious impediment for me. At that point, I set out specifically
searching for monad examples in Scala, and found this excellent series of blog posts by James Iry:Monads
are Elephants (see also parts two, three,
and four). Finally, monads were beginning to make sense! But after reading these articles,
I realized I was going to have to implement some monads myself to really grok them. I also knew that I would have to follow by example to do this. Which meant that I really needed to bite the bullet and look at some monad examples in Haskell.

No Nonsense Intro

I found an excellent exposition on monads in this Haskell
Wikibook, and set out to translate the examples into Scala. In this blog post, we will present a translation of the Maybe/Person example presented in the first
page the chapter on monads. In future blog posts, I hope we can do the same for the rest of
the pages of this chapter.

A Note on the Code

All the code presented below is based on the Understanding monads page of the Haskell
Wikibook. I am very grateful to the authors for making this available. It's very well written! I am also deeply appreciative of the fact that they provide this material under the terms of the Creative
Commons License. While I rely heavily on the code presented here, none of it is re-presented here. Most sections of this blog post provide links to the section of the page being discussed. It may help if you read through the wikibook chapter before tackling
the Scala presented here, but it isn't strictly necessary.

All the Scala code presented here is my own. The code can be found in this monads-in-scala project
on GitHub. I tagged the code as it is presented here as monads-in-scala-1, as I anticipate
modifying this code in future blog posts. Be sure to look at the code insrc/test/scala tree there as well. Launch sbt in the project,
run ~ test, play around and make some edits, and see what happens.

Implementing the Maybe Monad

Let's first translate the definition of the Maybe monad from Haskell into Scala. We'll present
the Scala solution first, and then we'll break it down:

1234567891011121314
sealed trait Maybe[+A] {   // >>=  def flatMap(f: A => Maybe[B]): Maybe[B]} case class Just[+A](a: A) extends Maybe[A] {  override def flatMap[B](f: A => Maybe[B]) = f(a)} // Nothing in the Haskel examplecase object MaybeNot extends Maybe[Nothing] {  override def flatMap[B](f: Nothing => Maybe[B]) = MaybeNot}

view rawMaybeNoMap.scala hosted
with ❤ by GitHub

Definition of Monad

A monad is defined in the wikibook, and in Wikipedia, as having three parts:

A type constructor
A unit function, commonly called return in Haskell
A binding operation, commonly called <<= in Haskell

How do these elements translate into Scala? Let's examine them one by one.

Type Constructor

In Scala, a type constructor is just the definition of a type that has a type parameter. In our case, the type definition is trait Maybe[+A]. The A is
the underlying data type. The + indicates covariance, which
means that if an A is a B, then a Maybe[A] is also aMaybe[B].
This is the typical type variance for a container where, once constructed, we can read out what is contained inside, but we cannot write in to the container. In other words, an immutable container.

Because MaybeNot is an empty container, we can use the same MaybeNot instance for all maybes, regardless of the type parameter.
To achieve this, we make the type parameter the special Scala type Nothing, which is a sub-type of every other type. Thanks to the covariance of A in Maybe[+A],
the type parameter of MaybeNot matches regardless of what is chosen for A.

We use MaybeNot here where the Haskell example uses Nothing, to avoid confusion with Scala's Nothing.

Unit Function

The unit function is a function from the contained type to the container type, i.e., from A toMaybe[A]. The unit function
is typically named return in Haskell. A natural place to put a function like this in Scala is as an apply method of a companion
object. Because apply methods are automatically generated by the compiler for case classes, we get our unit function for free in method Just.apply(A).

Binding Operation

The binding operation, as a Scala function, would have the following signature:

def bind[A, B](Maybe[A])(A => Maybe[B]): Maybe[B]
In Haskell, this is implemented as a function named >>=. As Scala is an object oriented language, it is natural to implement this as a method on type Maybe[A].
It's also customary to name such Scala methods flatMap.

As you can see, we make use of polymorphism in the above definition of flatMap. This might not please the hard-core functional programmers out there. A non-polymorphic implementation can be achieved
with the Scala match operator, and is a more direct translation of the Haskell example:

12345678
sealed trait Maybe[+A] {   // >>=  def flatMap[B](f: A => Maybe[B]): Maybe[B] = this match {    case Just(a) => f(a)    case MaybeNot => MaybeNot  }}

view rawMaybeNoPolymorphism.scala hosted
with ❤ by GitHub

Maybe Monad Motivation

The initial motivating example for Maybe in the wikibook is a family database where
lookups on father and mother may or may not return a result. The wikibook does not define the functions mother and father, but
we provide implementations of them in Scala, since we want to be able to test our code. We also provide a list of people to use for testing via companion object method Person.persons. The only
thing of note here is that we provide mother and father as instance methods, and not just regular functions:

12345678910111213141516171819202122232425262728
object Person {   val persons = List("P", "MP", "MMP", "FMP", "FP", "MFP", "FFP") map { Person(_) }   private val mothers = Map(    Person("P") -> Person("MP"),    Person("MP") -> Person("MMP"),    Person("FP") -> Person("MFP"))    private val fathers = Map(    Person("P") -> Person("FP"),    Person("MP") -> Person("FMP"),    Person("FP") -> Person("FFP"))    def mother(p: Person): Maybe[Person] = relation(p, mothers)    def father(p: Person): Maybe[Person] = relation(p, fathers)    private def relation(p: Person, relationMap: Map[Person, Person]) = relationMap.get(p) match {    case Some(m) => Just(m)    case None => MaybeNot  }} case class Person(name: String) {  def mother: Maybe[Person] = Person.mother(this)  def father: Maybe[Person] = Person.father(this)}

view rawPerson.scala hosted
with ❤ by GitHub

Maternal Grandfather

The wikibook goes on to explain the usefulness of the Maybe monad by defining amaternalGrandfather function using >>=.
What follows is an implementation ofmaternalGrandfather using flatMap, the equivalent version using match,
and a simple test showing the two functions produce the same results.

123456789101112
def maternalGrandfather(p: Person): Maybe[Person] =  p.mother flatMap { _.father } def maternalGrandfatherNoFlatMap(p: Person): Maybe[Person] =  p.mother match {    case Just(m) => m.father    case MaybeNot => MaybeNot  } Person.persons foreach { p =>  assert(maternalGrandfather(p) == maternalGrandfatherNoFlatMap(p))}

view rawmaternalGrandfather.scala hosted
with ❤ by GitHub

Both Grandfathers

A similar example follows for bothGrandfathers. This function returns Nothingunless both grandfathers are found. The match version
here is greatly simplified by the fact that we can match on two parents at once using pairs.

1234567891011121314151617181920212223242526272829
def bothGrandfathersFlatMap(p: Person): Maybe[(Person, Person)] =  p.mother flatMap { m =>    m.father flatMap { fm =>      p.father flatMap { f =>        f.father flatMap { ff =>          Just(fm, ff)        }      }    }  } def bothGrandfathersNoFlatMap(p: Person): Maybe[(Person, Person)] =  (p.mother, p.father) match {    case (Just(m), Just(f)) =>      (m.father, f.father) match {        case (Just(fm), Just(ff)) => Just((fm, ff))        case _ => MaybeNot      }    case _ => MaybeNot  } def assertBothGrandfathers(  bothGrandfathers1: Person => Maybe[(Person, Person)],  bothGrandfathers2: Person => Maybe[(Person, Person)]) =  Person.persons foreach { p =>    assert(bothGrandfathers1(p) == bothGrandfathers2(p))  } assertBothGrandfathers(bothGrandfathersFlatMap, bothGrandfathersNoFlatMap)

view rawbothGrandfathers.scala hosted
with ❤ by GitHub

To demonstrate the usefulness of matching tuples in Scala, let's rewrite the non-flatMapversion to avoid matching pairs:

12345678910111213141516171819
def bothGrandfathersNoFlatMapNoPairMatch(p: Person): Maybe[(Person, Person)] =  p.mother match {    case Just(m) =>      p.father match {        case Just(f) =>          m.father match {            case Just(fm) =>              f.father match {                case Just(ff) => Just((fm, ff))                case _ => MaybeNot              }            case MaybeNot => MaybeNot          }        case MaybeNot => MaybeNot      }    case MaybeNot => MaybeNot  } assertBothGrandfathers(bothGrandfathersFlatMap, bothGrandfathersNoFlatMapNoPairMatch)

view rawbothGrandfathersNoPairs.scala hosted
with ❤ by GitHub

Both Grandfathers Using For

The wikibook proceeds to rewrite the bothGrandfathers example
using a Haskell doloop. The Scala equivalent is a for loop, but to make the for loop
work, we need to go back and add a map method to Maybe. The equivalent operation to map in Haskell is >>,pronounced then.
We can define map in terms of the unit function and the binding operation:

12345678
sealed trait Maybe[+A] {   // >>=                                                                                                                                                 def flatMap[B](f: A => Maybe[B]): Maybe[B]   // >>                                                                                                                                                  def map[B](f: A => B): Maybe[B] = flatMap { a => Just(f(a)) }}

view rawMaybeWithMap.scala hosted
with ❤ by GitHub

Now we can proceed to write bothGrandfathers using a for loop:

123456789
def bothGrandfathersForLoop(p: Person): Maybe[(Person, Person)] =  for(    m <- p.mother;    fm <- m.father;    f <- p.father;    ff <- f.father)  yield (fm, ff) assertBothGrandfathers(bothGrandfathersForLoop, bothGrandfathersFlatMap)

view rawbothGrandfathersForLoop.scala hosted
with ❤ by GitHub

Monad Laws

The wikibook continues by explaining that the unit function and the binding
operationcannot be just any old operations; They must obey the three provided laws: left unit, right unit, and associativity. It's obviously not exhaustive and not a proof, but we provide some test code that exercises the three
laws with test values. We iterate over all the Personobjects in Person.persons to substitute for x.
For m, we substitute MaybeNot, plusJust(p) for every p in Person.persons.
We take instance method Person.motheras f, and Person.father as g.
Here's our test code to help convince ourselves that the monad laws are satisfied:

1234567891011121314151617
Person.persons foreach { p =>   // left unit                                                                                                                                          assert((Just(p) flatMap { _.mother }) == p.mother)} val maybes = MaybeNot +: (Person.persons map { Just(_) })maybes foreach { m =>   // right unit                                                                                                     assert((m flatMap { Just(_) }) == m)   // associativity  assert(    (m flatMap { _.mother } flatMap { _.father }) ==    (m flatMap { _.mother flatMap { _.father } }))}

view rawMaybeMonadLaws.scala hosted
with ❤ by GitHub

Monads and Category Theory

The wikibook page on understanding monads concludes with a brief foray into category
theory. Two extra functions on monads, fmap and join, are introduced. The Scala equivalent to fmap is map,
presented above. The Scala equivalent to join is flatten, which can also be defined in terms of the unit function and
the binding operation. However, flatten is tricky to define in Scala, because it requires that the type parameter A is
itself a monad. This is achieved in Scala by adding an implicit parameter with type <:<, like so:

123456789101112
sealed trait Maybe[+A] {   // >>=  def flatMap[B](f: A => Maybe[B]): Maybe[B]   // >>  def map[B](f: A => B): Maybe[B] = flatMap { a => Just(f(a)) }   // join  def flatten[B](implicit asMaybeMaybe: Maybe[A] <:< Maybe[Maybe[B]]): Maybe[B] =    asMaybeMaybe(this) flatMap identity}

view rawMaybeWithFlatten.scala hosted
with ❤ by GitHub

To understand this, let's start with the Scaladoc documentation for scala.Predef.<:<, which reads, "An instance of A <:< B witnesses
that A is a subtype of B. Requiring an implicit argument of the type A
<:< B encodes the generalized constraint A <: B." So the implicit parameter in flatten provides a sort of identity mapping
function that maps type Maybe[A] onto type Maybe[Maybe[B]]. The Scala library is designed so that the compiler will guarantee
that Maybe[A] is a sub-type of Maybe[Maybe[B]] at any pointflatten is
called. For instance, the following code:

Just(1).flatten
Will produce the following compiler error:

Cannot prove that maybe.Maybe[Int] <:< maybe.Maybe[maybe.Maybe[B]].
Within the body of flatten, the implicit parameter asMaybeMaybe acts as a type converter from Maybe[A] to Maybe[Maybe[B]].
After performing the type conversion, we simply apply the definition of join as presented in the wikibook. (The identityfunction
is provided in scala.Predef.) Here is a short code sample testing that theflatten implementation is correct:

1234567
Person.persons foreach { p =>  assert(Just(Just(p)).flatten == Just(p))}  assert(Just(MaybeNot).flatten == MaybeNot) assert(MaybeNot.flatten == MaybeNot)

view rawassertMaybeFlatten.scala hosted
with ❤ by GitHub

Monads as Functors

In our example code, we have defined fmap and join (methods map and flatten in
Scala) in terms of >>= and return (flatMap and Just.apply in
Scala). Theconcluding section of the wikibook goes on to explain that instead,
you could define >>=in terms of fmap and join. Let's see if we
can support this claim by writing an alternateflatMap function in these terms, and check that the results are the same as the originalflatMap:

123456
def altFlatMap(m: Maybe[Person], f: Person => Maybe[Person]): Maybe[Person] = m.map(f).flatten val maybes = MaybeNot +: (Person.persons map { Just(_) })maybes foreach { m =>  assert(altFlatMap(m, _.mother) == (m flatMap { _.mother }))}

view rawassertAltFlapMap.scala hosted
with ❤ by GitHub

Conclusion

It's been an interesting and educational exercise to work through translating these examples from Haskell to Scala. I've gotten a bit more comfortable with Haskell syntax, which should help me out in the future. But having programmed in Scala for over 2 years,
I'm already pretty comfortable calling methods like map, flatMap, and collect on
anOption. So I'm looking forward to translating the other monad examples in the Haskell wikibook. But before we do that, we'll have to work through two more Maybe examples
presented in the next section, which also introduces the <=< operator,
which chains functions that return monads.

at 20:06

Email
ThisBlogThis!Share
to TwitterShare
to Facebook

Labels: haskell, less-colon-less
operator, monads, monads
in scala, scala


翻译 monads-are-elephants 第一部分

发表回复

原文:翻译 monads-are-elephants 第一部分 | 在路上
http://hongjiang.info/monads-are-elephants-part1-chinese/
介绍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
Mdata 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 f
bind f mm >>= f

or

f =<< m
m flatMap f
dofor
[b]
脚注:

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分类,被贴了 monadsscala 标签。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: