Functors
我们首先定义 fmap
函数,其功能类似于列表的 map
。
其定义如下:
1 | class Functor f where |
可以举几个例子:
列表:
1 | instance Functor [] where |
Maybe
类型:
1 | instance Functor Maybe where |
简单来说,fmap
就是对一个 Functor 类型的元素中的值进行操作。
fmap
需满足以下条件:
$$
\texttt{fmap id = id}
$$
$$
\texttt{fmap (g . h) = fmap g . fmap h}
$$
Applicatives
Functor 中 fmap
的参数只能是一元函数。而 Applicatives 提供了一种拓展:
1 | class Functor f => Applicative f where |
这样,可以得到这样的结果:
1 | fmap0 :: a -> f a |
在这里,<*>
类似于 ->
和 $
,或者说是一种 Curry 化的方式。
当然,<*>
和 pure
也要满足一定的条件:
$$
\texttt{pure id <*> x = x}
$$
$$
\texttt{pure (g x) = pure g <*> pure x}
$$
$$
\texttt{x <*> pure y = pure (\g -> g y) <*> x}
$$
$$
\texttt{x <*> (y <*> z) = (pure (.) <*> x <*> y) <*> z}
$$
这些可能没有 Functor 的规则好理解。我们逐一解释:
$\texttt{pure id <*> x = x}$:保证
id
函数的恒等性质。$\texttt{pure (g x) = pure g <*> pure x}$:两种 Curry 化传参数的方式对结果没有影响。
$\texttt{x <*> pure y = pure (\g -> g y) <*> x}$:交换参数顺序,函数不变。
$\texttt{x <*> (y <*> z) = (pure (.) <*> x <*> y) <*> z}$:这个是最复杂的一个,我们忽视
f
的存在,尝试用->
等来改写这个式子:$$
\texttt{x -> (y -> z) = (x.y) -> z}
$$其实不过是函数复合而已。
Monads
定义
我们先写出如下代码:
1 | data Expr = Val Int | Div Expr Expr |
这段代码是为了解决求解过程中出现除数为零的情况而产生的。然而这段代码一点也不简洁,与创造 Haskell 的大佬的理念不符。我们尝试使用刚刚学过的 Applicative:
1 | eval :: Expr -> Maybe Int |
这段代码看起来不错,却有类型错误。函数将返回 Maybe (Maybe Int)
。
可以看到类型对我们的限制还是很大的,如果不是因为类型错误,这段代码就非常理想了。而 Monad 恰恰可以帮我们解决这个问题:
1 | class Applicative m => Monad m where |
在这里定义的 >>=
运算符也相当于 $
的作用,即 function application,然而它解决了此过程中 Maybe
的嵌套问题。
那么我们就可以如是定义 eval
函数:
1 | eval :: Expr -> Maybe Int |
而 Monad 还提供了更简洁的实现(语法糖):
1 | eval :: Expr -> Maybe Int |
你或许会疑心为什么这与 IO 中的 do
语句如此相似。事实上,IO正是 Monad 的实例:
1 | instance Monad IO where |
不过,这两个函数都是植入 Haskell 语言的,并不是通过 Haskell 后定义的,所以这里也没有他们的代码。在这里,当我们写出 c <- getChar()
这样的语句时,我们就将 IO Char
转化成了 Char
。
Monad laws
我们同样列出 >>=
需要满足的条件:
$$
\texttt{return x >>= f = f x}
$$
$$
\texttt{mx >>= return = mx}
$$
$$
\texttt{(mx >>= f) >>= g = mx >>= (\x -> (f x >>= g)) }
$$
State monad
现在我们考虑一个问题:用函数描述状态的变化。
首先定义状态,我们以 Int
类型为例,并定义 state transformer(简写 ST)来表示一个对状态进行转化的函数。同时,状态的变化还可能产生结果,我们将这三样东西如下定义:
1 | type State = Int |
另外,我们定义作用函数:
1 | app :: ST a -> State -> (a, State) |
然后,我们先将其声明为 Functor,然后是 Applicative,最后是 Monad:
1 | instance Functor ST where |
可以发现,我们定义的 <*>
是先将 g
作用于 s
,然后再用 st
作用于得到的状态。而 >>=
中则正好相反。事实上,<*>
的处理很容易理解,因为这符合我们一个一个传参数的顺序(也可以理解为上面的函数复合规则起的作用)。
Example: Relabelling trees
1 | data Tree a = Leaf a | Node (Tree a) (Tree a) |
我们现在要给如上定义的树进行重新标号,让每一个叶子节点都有不同的自然数编号。我们写出朴素的方法:
1 | rlabel :: Tree a -> Int -> (Tree Int, Int) |
现在我们用前面介绍的 State Monad 来改进这段代码:
1 | fresh :: ST Int |
Generic function
我们现在在 Monad 上定义一些通用的函数。在 Control.Monad
中提供了此类函数的一些实现。
1 | mapM :: Monad m => (a -> m b) -> [a] -> m [b] |
范畴论相关
我们现在探究 Functor 和 Monad 在范畴论以及在 Haskell 中的关系:
Functor
范畴论中的 Functor 是两个范畴间的关系,对于 $F:C\to D$,它包括:
$\text{object-mapping }C_0\to D_0$
$\text{morphism-mapping operation }f_{c\to c’}\to g_{F \ c\to F\ c’}$
并满足以下条件:
$F\ 1_c = 1_{F \ c}$
$F\ (g \circ f)=F\ g \circ F \ f$
这很显然与 Haskell 中 Functor 的条件是一样的;事实上,Haskell 中定义的 Functor 的实例 $\texttt{f}$ 即是 $\text{object-mapping}$,$\texttt{fmap}$ 即是 $\text{morphism-mapping operation}$
Kleisli Category and Monad
对于一个范畴 $C$ 及其自函子 $m:C\to C$,定义其对应的 Kleisli Category $K$:
$K_0\doteq C_0$
$K_0\doteq {f_{a\to b}\ |\ f’_{a\to m \ b}\in C_1}$
$\text{dom }f_{a\to b}\doteq a, \text{cod }f_{a\to b}\doteq b$
$1_a\doteq\text{return}_{a\to m \ a}\in C_1$
$g_{b\to c}\circ f_{a\to b}\doteq f’{a \to m \ b}\rightarrowtail g’{b\to m \ c} = h’_{a\to m \ c}$
条件:
- $\text{Unit: }f\circ1=f=1\circ f$
- $\text{Associativity: }h\circ(g\circ f)=(h\circ g)\circ f$
那么我们称 $m$ 为 Monad。那么我们可以如下定义 Monad:
1 | class Monad m where |
其中 >=>
运算符即是上述定义中的 $\rightarrowtail$。
当然,我们还可以进一步简化上述定义,比如前面介绍的 Haskell 中的 Monad 实践。