mostly:functional 第二十九章:Monad 的法则

梅贾德斯不是照人类传统的时间来记戴,而是着眼在一个世纪发生的生活故事,一切同时存在於一瞬间。

-- 加布列·贾西亚·马奎斯, 百年孤寂


又一次来到墙的前面。即使它还是半透明的,但却依然能感觉到那个悠久而质朴的感觉。慢慢延着墙散步,看着其上各式各样的痕迹,可以感受到有多少想穿过它的尝试。

但那座墙上是有个门的。而我现在看得到了。

我朝着门走去,而那本书也跟了上来:

Monad 的实作:

class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
{-# MINIMAL (>>=) #-}

Monad 的法则:

  1. 单位元素
  2. 结合律



Monad 不是些什麽

在谈 Monad 是什麽之前,我认为更加重要的,是先确定一下 Monad 不是什麽

  • 不是副作用
  • 不是 Haskell 里执行指令式程序设计 (imperative programming) 的专门语法。
  • 不是一个值
  • 不是积极执行 (strictness)
  • 不是设计模式

在参悟的过程中,觉得有点怀疑的时候,可以回头来对照一下这张表。

当用来 fmap 的函式也是回传一个串列时

让我们回到串列那个大家都爱用的 map 来看一下:

-- Haskell 语法
f = (+1)

map f [1, 2, 3] -- => [2, 3, 4]

那麽如果我们有个函式 g,输入一个数字,就会回传一个串列的话:

-- Haskell 语法
g x = [x, x + 1, x + 2]

g 10 -- => [10, 11, 12]

那麽我们用 g 这个函式,对一个串列进行 map 时,会回传……串列包着的串列:

-- Haskell 语法

map g [1, 2, 3] -- => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

坍缩成一层

很多时候,我们会希望上面那个串列的串列,是能自动坍缩成一层的阵列的。如果你很习惯用其它语言里的 map,那麽你八成也知道有一个这种特性的函式: flat_map

# Elixir 语法
g = fn x -> [x, x + 1, x + 2] end

Enum.flat_map([1, 2, 3], g)
# => [1, 2, 3, 2, 3, 4, 3, 4, 5]

而在 Haskell 里,你要用 concat 这个把串列的串列摊平的函式,与 map 一起来做到这样的事:

-- Haskell 语法
concat [[1, 2], [3, 4]] -- => [1, 2, 3, 4]

concat $ map (\x -> [x, x + 1, x + 2]) [1, 2, 3]
-- => [1,2,3,2,3,4,3,4,5]

嗯。这个就是 Monad 的特性。

return?

许多人在看到型别定义里,那个叫 return 的函式时,通常会非常疑惑。因为绝大部份的程序语言里,都把这个字当做是终止目前的计算,并回传结果用的关键字。但是仔细看一下,在 Monad 里,这个 return 只是一个非常普通的函式。而它的型别是:

 return :: a -> m a

仔细看一下,它跟 Applicative 里的 pure 基本上是同一个东西。它不会终止什麽计算,也没有什麽特殊的行为。就是个单纯的拿到一个值,把值装进容器里的函式而己。

但是之所以选这个字,是有其用意的,之後我们就会看到了。

Monad:不只能坍平串列

当然 Monad 重点并不在於把嵌叠的串列坍成一层而己。如果谈论的对象只是串列的话,那就用 flat_map,或 concat $ map 来理解就可以了。而 Monad,既然是一个 typeclass ,那麽理解的重心,则是放在坍缩成一层这个概念,以及有哪些容器也能有这个坍缩成一层的特性,接着会延伸出这个特性的不同使用手法。

让我们先来看 Monad 最重要的 >>= 函式,大家称它为 bind。这个函式先接收一个 Monad f a,再接收一个函式 (a -> fb),最後会回传包在单层容器里的 f (b)

为了要看这一系列 typeclass 的函式型别变换,我们把 >>= 改成用方向相反的 =<< 来表示,这个反过来的 =<< 则是一个先接收函式,再接收 Monad 的函式。

另外我们依序列出从函式应用 $、Functor 的 <$>(fmap 的中缀形式)、Applicative 的 <*>与 Monad 的 =<< 的型别,一个个往下排,并加上空白看看:

-- Haskell 语法
$   ::                    (a -> b) ->   a ->   b

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

<*> :: Applicative f => f (a -> b) -> f a -> f b

(=<<) :: Monad f =>     (a -> f b) -> f a -> f b

--- 参考用: 原本的 >>= 的型别
(>>=) :: Monad f =>     f a -> (a -> f b) -> f b

我们之前已经看过,从 $<$>,第一个参数的函式保持不变,而接收的第二个参数,从处理单纯的值,变成可以处理装在容器里的值,是所谓的升格

而从 <$><*>,第一个参数的函式,也被放到容器里。而我们也说过,其行为可以看做容器里的两个值(一个函式,另一个是引数),用函式呼叫产生结果,而两边的容器外壳,则会像是 Monoid 那样结合 (mappend) 在一起。

而当我们谈到 =<<,如果只是单纯的用 fmap/<$> 将第一个参数的 (a -> f b) 的函式,应用到f a 里的话(注意 a 被装在 f 容器里)。那麽其结果的型别会是:f (f b) (想一下两层的串列)。而 =<< 的特性,就是它会把这个两层相同f 容器坍缩成一层,让结果是 f b

是的。 Monad 的本质,就是这样而己。




当我正想再去找万用钥匙时,发现这次门上面的标识不太一样。开孔上方,写着这样的字:

join :: m (m a) -> m a

所以是想要一个可以把双层的容器坍缩成一层容器的函式罗?我仔细想了一下……

[to be continue]


<<:  [DAY 30] 复刻 Rails - View 威力加强版 - 2

>>:  Day 29 - Summary

DAY 16 - 树怪

大家好~ 我是五岁~ 今天来挑战一个树怪吧~!! 目标就是一棵树~ 阿不是...是一个树怪~ 不想要...

Day14 Sideproject(作品集) from 0 to 1 - 前端专案架构

在很多很多的前置作业後 今天终於要开始写code了 到此为止我们应该流程画有了 画面流程也有了 今天...

[DAY-08] 增进诚实敢言 把一切摊在阳光下

人们如果主动隐瞒某些事 反而会花两倍时间想着那着些事 秘密的问题在於 只要你说出来 他就不再是秘密...

Day 0x1E UVa11321 Sort! Sort!! and Sort!!!

Virtual Judge ZeroJudge 题意 真.排序题 输入数字,按照要求输出排序後的结...

DAY2 - 排序(一)

今天介绍插入排序法&快速排序法~~ 主题还是希望围绕在实战刷题,毕竟刷题的时候有需要排序大多...