Monad入门

当你觉得不能理解某个概念的时候,

其实是没有和已知的概念建立联系。


不要懊恼,也不要急躁。

应该耐心的从基础学起,欲速则不达。

Only people who die very young learn all they really need to know in kindergarten.

只有早死鬼才指望在幼儿园中就学会一切。

——《Expert C Programming》


Monad是Haskell中的一个概念。

你也可以说它是一个数学概念,

但我们今天不谈数学,只谈Haskell,从简单的入手。


要到达Monad的环节,可不是容易的事情。

我们需要逐步学会一系列全新的概念。

这也是为什么求快的人总是学不会的原因了。


我们就开始吧。


值和类型

Haskell中表达式是有值的,

比如,值为1,值为'a',

他们在内存中存储方式是一样的。


但是,感觉他们应该是不同的东西。

应该使用类型进行区分。


我们称值1的类型是Int,整型

值'a'的类型为Char,字符型


这样的话,实际上是在值这个概念之上建立了一层抽象。

不同的值因此有了不同的划分。


函数

函数用来将一个或多个值变成另一个值。

函数也是一种值,它也是有类型的。


例如,加法函数add是把两个整型值,变成一个整型值。

假如已经定义好了add函数,那么Haskell中如下调用它。

add 1 2

= 3


现在,我们考虑一个问题。

如果,我们只为add提供了一个参数,结果是什么。

即,add 1是什么?


add就好像一台有两个插槽的机器,

如果插了两个槽分别是1和2,那么它会弹结果3出来。

如果只插一个槽,那么它还是一台机器,一台只有一个插槽的机器。


所以,add 1还是一个函数,只不过它只接受1个值作为参数。

它会返回这个值与1相加的结果。

也就是说,add 1,把一个整型值变成了另一个整型值。

我们记add 1的类型为Int -> Int


接着,我们看add的类型是什么?

add既可以看做一台有两个插槽的机器,这台机器插满两个槽以后,弹出结果。

还可以看做一台有一个插槽的机器,插完这个槽以后,返回一台有一个槽的机器。

例如,add用1调用返回了add 1


所以,add的类型应该是Int -> (Int -> Int)

因为,对于任意一个多参数函数,我们总是可以这么想。

干脆把括号也去了,写成Int -> Int -> Int


像以上这样的看待函数的方式,称为柯里化。

后面我们将看到,Haskell中不仅函数,很多带多参数的东西都可以柯里化一下。


自定义类型

我们正在处理的值1,2,3和2014,2015,2016,看起来是不同的。

用同一个Int类型来表示,有点牵强。

因为实际上,我们相用1,2,3表示月份,而用2014,2015,2016表示年。


能提供自定义的类型就好了。

比如1,2,3是类型Month的值,而2014,2015,2016是类型Year的值。


Haskell中提供了关键字data用来自定义类型。

例如:

data Month = MonthValue Int

data Year = YearValue Int


其中,Month表示类型,MonthValue表示值构造器,它用来构造这个类型的值。

例如:

MonthValue 1就是一个Month类型的值了。


我们看到值构造器MonthValue带了参数Int,表示这个类型有一个字段。

值构造器还可以不带参数,也可以带多个参数。

这里要注意,Int是一个类型。


例如:

data Bool = True | False

其中“|”是表示或者的意思。


Bool类型有两个值构造器,分别是True和False,它们不带参数。

即,True就是一个Bool类型的值了。


再例如:

data Date = DateValue Year Month Day

其中,Date新定义的类型,DateValue是值构造器,

而Year,Month,Day都是类型。

它表示Date类型的值由值构造器DateValue来创建,具有3个字段。


然后,麻烦的地方来了。

Haskell就是有这个特点,几乎什么东西都可以带参数。

带了多个参数以后,又都可以柯里化。


现在我们让类型带上参数。

例如:

data Maybe a = Nothing | Just a

其中,Maybe是带参数的类型,称为类型构造器。

Nothing如上所示,它是一个不带参数的值构造器。

Just如上所示,它是一个带参数的值构造器。

“|”是表示或者的意思。

a表示一个参数类型。

注意:带参数的类型和类型构造器是一个概念,后文要经常用到。


对于类型构造器,可以这样理解。

Maybe Int才是一个具体的类型。

它有两个值构造器Nothing和Just Int。


我们当然还可以定义两个参数的类型构造器。

data Either a b = Left a | Right b


这样理解,

Either Int Char才是一个具体的类型,

它有两个值构造器,Left Int和Right Char


刚才我们提到了,

Haskell中遍布柯里化的思想。

在这里,Either Int Char是一个具体类型。


如果只提供了参数类型Int呢?

结果Either Int就是个接受一个参数的类型构造器了,即,单参类型构造器。

因为Either Int还需要一个具体类型,比如Char,才能得到具体的类型Either Int Char


到这里,类型就介绍完了。

看起来还是容易理解的。


然后,我们提一下Haskell中故意让你引起混淆的地方。

Haskell中,值构造器可以和类型构造器同名。

也就是说:

data Month = Month Int

Month到底是什么,根据上下文来定。


类型类

有过面向对象编程经验的人们,对以上还好理解。

因为至少名字在经验中是有的。


而类型类却是一个全新的概念。

一开始学习时,我认为将它和其他编程语言中的任何概念进行对比,都是糟糕的主意。

而等我们学到了一定程度,至少知道了这个概念到底是什么之后再联想,这才是好办法。


其实以上介绍的“类型”,也尽量先不要类比面向对象中的类型。


任何概念的引入都是有原因的。

我们发现原因才能学到精髓。


就像类型是为了对值进行划分一样。

类型类是为了对类型进行划分。


为什么要这样做呢?

因为,我们想要用同一个函数作用在相同的类型类的不同类型上。


例如:判断相等的函数,我们应当既可以比较Int,也可以比较Char

如果用来比较Int,相等函数的类型是

intEqual :: Int -> Int -> Bool

如果用来比较Char,相等函数的类型是

charEqual :: Char -> Char -> Bool


我们不得不为它们设置不同的名字进行区分,

因为类型不同,它们不是同一个函数,

这太麻烦了。


所以,Haskell就想了一个办法,

将类型进行划分,比如我们把能够进行比较操作的类型都划分到相等类型类Eq中。


那么Int类型和Char就称为Eq类型类的实例。

相等函数就可以设置同一个名字了。


例如:Eq类型类可以这样定义。

class Eq a where

    equal :: a -> a -> Bool


类型类的实例可以这样声明。

instance Eq Int where

    equal i j = i == j


这里class和instance,可不是面向对象中的概念呀,虽然勉强有点相似。


目前为止还是很容易理解的。

其中a表示类型,i j是值。


又出现了一个Haskell中故意让你引起混淆的地方。

equal i j = i == j也可以写成equal a b = a == b

a到底是什么,根据上下文来定。


Monad

我们现在有了足够的经验来看Monad了。

Monad是什么呢?


Monad是一个类型类。

就这么简单。


可是,如果不理解类型,类型类,怎么理解Monad呢?

所以,我们只能循序渐进。


我们看一下Moand的定义吧。

看看还能不能发现新的东西。


class Monad m where

    (>>=) :: m a -> (a -> m b) -> m b

    return :: a -> m a


可以看到,Monad类型类为它包含的各个类型,定义了相同的函数>>=和return。

这里return也不是表示函数的返回值,没有学会之前先不要类比。


我们来分析一下它们的含义吧。

先看return,

类型类的定义中,return :: a -> m a

表示了return这个函数的类型。


所以,a是一个类型,m a也是一个类型。

m a怎么可能是一个类型呢?

因为m是一个类型构造器,它是带一个参数的类型构造器,即,m是一个单参类型构造器。

比如我们定义的data Maybe a = Nothing | Just a

Maybe就能以m的身份出现。


另外,class Monad m where表示m是类型类Monad的实例。

所以,Monad类型类的实例,必须是带一个参数的类型。


然后,我们看>>=函数

(>>=) :: m a -> (a -> m b) -> m b

让我们想起了add函数的类型

Int -> Int -> Int


add函数接受两个Int类型的值作为参数,返回一个Int类型的值。

函数>>=看起来则是,接受m a类型的值,和a -> m b类型的值,返回m b类型的值。


其中,m是单参类型构造器,a -> m b表示一个函数,a和m a和m b都表示一个具体的类型。


那么>>=和return到底是干什么的呢?

为什么要定义这么古怪的函数呢?


其实我们可以我们可以把m看做一层“壳子”

在>==操作过程中,这层“壳子”是不改变的。

return :: a -> m a

表示我们把一个a类型的值“加壳”变成了m a类型的值。

(>>=) :: m a -> (a -> m b) -> m b

表示给我一个m a类型的值,和一个函数a -> m b,我就给你一个m b类型的值。

a -> m b类型的函数,表示把a类型的值变成m b类型的值。


函数>>=在顺序操作中是很有用的。

我们看到m a最终变成了m b,“壳子”还是有的。

我们再对m b进行操作让它变成m c,“壳子”也还是有的。


也正因为如此,在Haskell中,通常称m a类型的值是一个monad值,

而其中的类型a的值,通常称为monad值中“包含”的值。


入门

写到这里,我们才对Monad的概念有了初步的了解。

勉强算是入门了。


因为,首先,对于Monad的应用我们还没有了解。

其次,只是Monad类型类的实例,并不是一个真正意义上的monad,还要遵循Monad定律。


但是相信本文能对继续学习Haskell带来帮助。


最后,我们再提一个柯里化的应用。

记得我们定义了Either类型,它的类型构造器有两个参数。

data Either a b = Left a | Right b


Either类型可以是Monad类型类的实例吗?

不行。


因为Monad类型类的实例必须是一个单参类型构造器,即,接受一个参数的类型。

而Either接受两个。


回想我们对Either的介绍。

Either Int它是一个单参类型构造器。

所以,Either int就可以是一个Monad类型类的实例了。


Int其实也不必是具体类型,我们其实想表示Either a可以是Monad类型类的实例,

即,instance Monad (Either a) where


你还能Hold住吗?