Functions as Monad


思来想去,觉得看过这本小书之后自己真正掌握的东西不多,并不敢贸然地去写些什么,但是我相信任何看过的人都不会一点感悟没有的。抱着这样略显复杂的心情,准备写点没什么深度的文字以备来日复习(一些概念每次再看都会重新变得新鲜),或许之后回来看时会觉得有些地方写得很不妥吧!

看了趣学指南,学到了许多新奇而有趣的概念。谈到这一点,最多的恐怕是类似 “如何理解 Monad?”、“Applicative 与 Monad 有什么关系?” 此类帖子。如果单是说到 Haskell 里的这些概念,我不能更同意去把它们理解为一种抽象,毕竟搞玄学不是什么好事(也不能这么说…毕竟去看 category theory 的话会有帮助)。在学习此类概念的过程中,Adit 搞的那些图解是很有些帮助的,但是比较遗憾的是里面用 Maybe 作例子太多了,其实最让人困惑的应该还是 Function Monad,因为它的上下文并不那么单纯。

先来看一看这些 Typeclass 好了,首先是 Functor

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor ((->) r) where
  fmap f g = (\x -> f (g x))

然后是 Applicative

class (Functor f) => Applicative f where 
  pure :: a -> f a    -- put a value in context
  (<*>) :: f (a -> b) -> f a -> f b 

instance Applicative ((->) r) where
  pure x = (\_ -> x)
  f <*> g = \x -> f x (g x)

最后是主角 Monad

class (Applicative m) => Monad (m :: * -> *) where
  return :: a -> m a  -- just like pure function
  (>>=) :: m a -> (a -> m b) -> m b  -- bind
  (>>) :: m a -> m b -> m b
  fail :: String -> m a

instance Monad ((->) r) where
  return x = \_ -> x
  h >>= f  =  \w -> f (h w) w

稍稍查资料就会清楚,虽然定义是这么来的,但其实 Monad 并不依赖于 Functor 与 Applicative(因为众所周知的 ap 和 return 等),对于函数作为实例也是这样,不过可能最好还是从 Functor 说起比较好。

还记得对于函数作为 Functor,有这个推导:

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

已经知道 Functor 喻示了“可映射”这个行为,Maybe 和 [] 自然不必说,但是很显然的是函数作为 mapping 也应该是可映射的,这也就说明了 fmap 作用在函数上就相当于函数组合。如果回想一下数学课本上表示映射的那张图,仿佛一切都是理所当然的了!

到这里一切都很顺利,清楚了 <$>(也就是 fmap) 是函数组合,遇到它时一下子就能理解完毕。接下来进入到 Applicative 时可能会有些麻烦,我们会给 function 外面套一个壳,如果这个壳本身也是 function 会怎样呢?

这时候表达式很可能会是 <$> 与 <*> 所连成的,比如这样:

(+) <$> (+3) <*> (*100) $ 5

这个东西看起来不怎么顺眼,但是比较清楚的是我们把 5 应用到了一个函数上,然后会得到一个 Num 值。不过在这里趣学指南的说明就非常的模糊了,似乎并没有捋清楚。仔细看一下 (+) <$> (+3) 就会发现这是一种函数组合,它构成了这样一个函数:接受一个 value,返回一个函数。如果我们给它应用上一个名为 val 的值,将结果用 lisp 风格写出来会看得更加清楚:

(+ (+ 3 val))

没错!根据 Currying 来理解,这就表明我们已经把那层壳下面的函数给取出来了。接下来让它和 (*100) 这个函数作用在一起,依然是函数复合!如果我们把它们看成整体,并用 val 应用的话,就会有下面的表达形式:

((+ (+ 3 val)) (* 100 val))

稍微观察一下,其实这个 val 就是我们用来应用的数 5,把它代入上式就会得到结果 508 了。不过等一下,现在可以发现这个结果好像就像是书里所表述的那样,先把 5 应用到两个函数 (+3) 和 (*100) 上,再让二元函数 (+) 应用在它们的结果上面。按照这种思路来理解的话,我们就相当于自己发现了 liftA2!

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

Applicative 为我们缓冲了一下,最后再来看看它是怎么成为 Monad 的(据说 Monad 可以被看成是 Applicative 的威力加强版)。Monad 依然表现为一层壳,我们把处于函数上下文中的值丢进 (»=) 去处理,然后它会连同一个“返回函数的函数”一起被加工一番,最后我们得到的也是一个函数,可以写一个例子来看一下:

(+3) >>= (+) >>= (*) >>= (+) >>= (*) :: (Num b) => b -> b

很长的一大串东西,看起来就让人有些抗拒,不过可以试着先给它喂一个值看看结果。把 5 应用上去,得到了 350 这个结果,把 1 应用上去,得到 6 这个结果。感觉好像还不是很清晰?实际上我们可以先考虑短链,依次用 (:t) 检查类型不难发现所有长度的链都拥有同样的类型描述。那么这意味着什么呢?回头看一下作为 monad 的函数的实例定义:

h >>= f  =  \w -> f (h w) w

是的,好像没有什么不清晰的地方。先塞一个值给 h 函数,然后再把结果值作为 r 塞进裹着 monad 的函数 f 里去把里面的函数给放出来,又成为了一个和 h 类似的函数了。这么一来这个表达式的求值形式似乎已经相当清楚了,可以再用 lisp 风格写一下:

(* val 
  (+ val
    (* val
      (+ val
        (+ 3 val)))))

现在来把 5 和 1 都代入进去就可以看到这个分析没有问题:

(* 5 (+ 5 (* 5 (+ 5 (+ 3 5)))))
(* 1 (+ 1 (* 1 (+ 1 (+ 3 1)))))

到目前为止还算清晰地把函数作为几个 Typeclass 的实例给反省了一遍,不过还有一个重要的问题没有解决,那就是独特的 do 记法。如果不能清楚地理解它作为语法糖而存在,可能会引起一些不必要的困惑,这里我觉得还是最好再举例子来看一看。

先是一段熟悉的代码:

addStuff :: Int -> Int 
addStuff = do 
  a <- (*2)
  b <- (+10)
  return (a + b)

显然,看函数签名就能发现它的类型了:接受一个 Int 值然后再给返回一个。比较糟糕的是,如果被某些语言荼毒过深,可能就会在看函数定义的时候发生许多困惑。这里最可能引发的困惑大概是无法理解 addStuff 能够作为一个函数而存在,却找不到参数从哪里传进去的。

给它变一变形就好了:

(*2) >>= \a -> (+10) >>= \b -> return (a + b) :: (Num b) => b -> b

这种操作恰巧我们刚对付过,很容易就可以知道求值结果,看到最后是一个 return 作用下的值,差不多就能清楚求和结果被放在了最小上下文里:

\_ -> (a + b)

那么,这一篇也差不多要结束了。看上去大概是以拙劣的文笔描述了一下理解性的东西。写到这里又往上看了一下,发现自己好像说了点东西,又好像什么也没说明…反正大概写的这些东西并不是关键,而恰好是基础中的基础吧。由于篇幅问题还有一些重要 Monad 的相关感想没有说到,今天只好先捋下 Reader Monad 罢了。