当你觉得不能理解某个概念的时候,
其实是没有和已知的概念建立联系。
不要懊恼,也不要急躁。
应该耐心的从基础学起,欲速则不达。
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住吗?