如何解释 Haskell 中的单子?

我就不说让大妈理解了,能让本科生理解就行了。
关注者
373
被浏览
40232

18 个回答

既然题主想要容易理解的回答,我猜题主可能是还在学习阶段。那我就尽量用最简单的语言解释单子(Monad)及其应用场景。

结论:
在haskell中,Monad也只是一个有自己特殊性质的typeclass。如果想要一个类型是monad的,必须实现Monad类型类,而实现Monad typeclass,只需至少实现(>>=)函数即可。
(>>=) :: m a -> (a -> m b) -> m b
有了这个概念,你就可以实现自己的monad了。

而什么时候要把类型定义为functor?什么时候定义为applicative?什么时候定义为monad?把握以下几个点:

functor类型解决的是如何将一个in context的value(如Just 1)输入到普通function中(如(+2) ),并得到一个in context的value(如 Just 3)。例子:
fmap (+2) Just 1

applicative类型解决的是如何将一个in context的value(如Just 1)输入到只返回普通value的in context的function中(如Just (+2) ),并得到一个in context的value(如Just 3)。例子:
Just (+2) <*> Just 1

monad类型解决的是如何将一个in context的value(如Just 3)输入到只返回in context的value的function中(如double x = Just (x * 2) ),并得到一个in context的value(如Just 6)。例子:
Just 3 >>= double

------------------------------------------------------
要想理解Monad,最好先理解两个相关的概念:Functor和Applicative。

我们通过一个例子引入:对于一个常数1,我们可以将其输入函数 (+2):
> (+2) 1
> 3
很简单,没有任何问题。

那么,如果常数1是在一个context里呢?比如是在Maybe类型里:
> data Maybe a = Just a | Nothing
那么,1会被表示成Just 1, 这时如果简单的用
> (+2) Just 1
就会报错:
Non type-variable argument in the constraint: Num (a -> Maybe a)
怎么办?

1. Functor
对于上面的问题,我们可以用fmap来解决。fmap可以从context里提取出value,计算,再将计算出的值放回context
> fmap (+2) Just 1
> Just 3
我们得到了结果Just 3.
为什么fmap可以从context中提取value?它是怎么做到的?规则是什么?

我们先看fmap函数出自哪里:
> :t fmap
> fmap :: Functor f => (a -> b) -> f a -> f b
那么Functor是什么?
我们再去查Hoogle:
The Functor class is used for types that can be mapped over.
class Functor f where

Minimal complete definition

fmap


Methods

fmap :: (a -> b) -> f a -> f b

(<$) :: a -> f b -> f a


......

可以这样理解这段定义:对于一个Functor类型的数据类型f,至少需要实现fmap。对于fmap函数,其中第一个输入(a -> b)是一个普通函数,比如(+2);第二个输入f a是一个Functor,比如Just 1;最后返回一个Functor,比如Just 3

对于Functor这样的类,我们称之为typeclass(类型类)。typeclass有点像Java的interface:实现这个接口要实现其中的方法。,

回到我们的例子,Maybe类型其实是Functor类型的。我们可以查实现Maybe的源代码去验证一下:
instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)
的确如此。这就是为什么运行fmap (+2) (Just 1)可以得到Just 3。

所以总结一下,Functor可以解决的问题是,当value被放在一个context中的时候(比如Maybe类型的Just 1),我们如何将其输入一个普通的function运算(如(+2) ),并得到in context的输出(如Just 3)

那么我们进一步考虑,如果function也被放在了一个context中了怎么办?

2. Applicative
比如这样的一个function:
Just (+2)
如果想将Just 1输入,直接写
> Just (+2) Just 1
肯定是不行的。如何解决这个问题?

我们可以用函数<*>来解决。
> Just (+2) <*> Just 1
> Just 3
同样,如果去查<*>来自哪里,我们会发现它是Applicative的一个方法:

A functor with application, providing operations to

  • embed pure expressions (pure), and
  • sequence computations and combine their results (<*>).
class Functor f => Applicative f where
(<*>) :: f (a -> b) -> f a -> f b
......
也就是说,一个Applicative类型的数据类型,必须实现<*>方法。这和Functor是一致的,Applicative也是一种typeclass。同样我们可以通过Maybe类型的实现来验证它也是Applicative类型的:
instance Applicative Maybe where
    pure = Just

    Just f  <*> m       = fmap f m
    Nothing <*> _m      = Nothing

    Just _m1 *> m2      = m2
    Nothing  *> _m2     = Nothing

总结一下,Applicative可以解决的问题是,如果当value被放在一个context中(如1被放入Maybe类型,变为Just 1),并且function也被放在一个context中(如(+2)被放入Maybe类型,变为Just (+2))时,如何运算并得到输出

我们注意到,<*>函数的value是在右边,function是在左边的。
那我们进一步考虑,如果我们想要value在左边,并且函数的输出是一个in context的值(比如Just 3这种形式)呢?

3. Monad
上面的情况,是可以用>>=函数(也叫bind函数)来解决的。
举个例子,假如我们有一个double函数:
double x = Just (x * 2)
它的输出是Maybe类型的。
那么我们想将Just 1进行double怎么做?用以下的代码即可:
> Just 1 >>= double
> Just 2
我们得到了Just 2。没错,>>=函数是Monad的一个方法,其定义为:
(>>=)  :: m a -> (a -> m b) -> m b
而Monad的定义如下:
The Monad class defines the basic operations over a monad, a concept from a branch of mathematics known as category theory. From the perspective of a Haskell programmer, however, it is best to think of a monad as an abstract datatype of actions. Haskell's do expressions provide a convenient syntax for writing monadic expressions.

class Applicative m => Monad m where

Minimal complete definition

(>>=)


Methods

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

(>>) :: forall a b. m a -> m b -> m b

return :: a -> m a

fail :: String -> m a

从定义中我们可以看出,Monad也是一个typeclass,那么作为typeclass,一个monad类型的数据类型,必须实现其方法。Monad本身定义里写的很清楚,“这是一个来自范畴学的概念,不过从编程角度看最好将其视作一组行为的抽象数据类型”。然而这句话到底怎么理解?

说白了,在haskell里,一个monad就是一个参数化的类型m,以及一个必须实现的函数 (>>=)
(>>=) :: m a -> (a -> m b) -> m b
没了。
举个例子,如果我们的参数化类型m是Maybe,(>>=) 按照
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
来实现,那么我们得到的就是一个Maybe monad。

monad有很多好的性质,比如如果我们想连续多次做double操作,用Applicative就会有点麻烦,但是用Monad就很简单:
> Just 1 >>= double >>= double >>= double >>= double
> Just 16

当然,monad能做的事情远不止这么一点,相关也还有其他很多概念,比如将monads结合起来的方法monad transformers,不过这就是另外一回事了。


结论:
在haskell中,Monad也只是一个有自己特殊性质的typeclass。如果想要一个类型是monad的,必须实现Monad类型类,而实现Monad typeclass,只需至少实现(>>=)函数即可。
(>>=) :: m a -> (a -> m b) -> m b
有了这个概念,你就可以实现自己的monad了。

而什么时候要把类型定义为functor?什么时候定义为applicative?什么时候定义为monad?把握以下几个点:

functor类型解决的是如何将一个in context的value(如Just 1)输入到普通function中(如(+2) ),并得到一个in context的value(如 Just 3)。例子:
fmap (+2) Just 1

applicative类型解决的是如何将一个in context的value(如Just 1)输入到只返回普通value的in context的function中(如Just (+2) ),并得到一个in context的value(如Just 3)。例子:
Just (+2) <*> Just 1

monad类型解决的是如何将一个in context的value(如Just 3)输入到只返回in context的value的function中(如double x = Just (x * 2) ),并得到一个in context的value(如Just 6)。例子:
Just 3 >>= double

回答很浅薄,甚至有很多地方是通过现象倒推原因,但希望能帮到题主理解Monad的概念。

-------------------------------------
在其他答主的评论区看到一个很有意思的问题:为什么大多数教材都用Maybe类型来作为functor, applicative和monad的举例?因为Prelude实现了Functor Maybe, Applicative Maybe和Monad Maybe,所以Maybe既是functor,又是applicative,还是monad,举例最方便嘛。
具体一些,说类型 M<T>(还是用各位熟悉的符号吧)是一个关于 T 的 Monad,就是说它和一个「算出一个 T 型数据的过程」差不多。请注意 Monad 只有「算出来」这部分,如果和函数类比的话它没有传参这一部分。在 Haskell 这类语言中,带有传参、带有返回的「计算步骤」是用一个返回 Monad 的函数表示的。

所有的 Monad 都要求定义两个算符:
  • M<T> return(T x),表示一个「直接返回」的步骤
  • M<T> operator>>=(M<P> first, (P -> M<T>) f_second),表示将两个步骤相连,并将前一步的结果传递给下一步。注意这里「第二步」不是简单的 Monad,而是一个返回 Monad 的函数,因为 Monad 表示的「计算步骤」只包含「算出结果」,不包含「读取参数」。
同时这些算符要满足一些公理:
  • (return x) >>= f 等价于 f x
  • m >>= return 等价于 m
  • (m >>= f) >>= g 等价于 m >>= (x => f(x) >>= g)。这句话实际上就说 >>= 满足结合律……
然后呢,那群程序员发现有太多的类型 M<T> 都可以定义出这两个算符,甚至连数组都可以。当然,Monad 最常见的用处还是表示计算步骤,进而用来处理程序流程和副作用。