粗略地讲,Haskell使用一个类别进行范畴论,其对象是Haskell类型,箭头是这些类型之间的函数。它绝对不是用于建模范畴论的通用语言。
(数学上的)函子是一种将一个类别中的事物转化为另一个可能完全不同的类别中的事物的运算符。而自函子则是一种在相同源和目标类别上的函子。在Haskell中,函子是一种将Haskell类型范畴中的事物转化为该范畴中的其他事物的运算符,因此它总是一个自函子。
(如果您正在跟踪数学文献,则技术上,“(a->b)->(m a->m b)”操作只是自函子m的箭头部分,“m”是对象部分。)
当Haskellers谈论“在单子中工作”时,他们实际上是指在单子的Kleisli类别中工作。单子的Kleisli类别起初非常令人困惑,并且通常需要至少两种颜色的墨水来进行充分的解释,因此请以以下尝试为基础,并查阅一些参考资料(遗憾的是,Wikipedia在这里除了直接定义外毫无用处)。
假设您在Haskell类型类别C上有一个单子“m”。它的Kleisli类别Kl(m)与C具有相同的对象,即Haskell类型,但是Kl(m)中的箭头a~(f)~>b是C中的箭头a-(f)->mb。(我在我的Kleisli箭头中使用了波浪线来区分两个箭头)。请再次强调:Kl(C)的对象和箭头也是C的对象和箭头,但箭头指向Kl(C)中的不同对象而不是C中的对象。如果这不让您感到奇怪,请仔细再读一遍。
具体来说,考虑Maybe单子。它的Kleisli类别只是Haskell类型的集合,其箭头a~(f)~>b是函数a-(f)->Maybe b。或者考虑(State s)单子,其箭头a~(f)~>b是函数a-(f)->(State s b)== a-(f)->(s->(s,b))。无论哪种情况,您始终将波浪箭头写成简写形式,以对目标域的类型执行某些操作。
(请注意,State不是单子,因为State的种类为* -> * -> *,因此您需要提供其中一个类型参数以将其转换为数学单子。)
到目前为止,一切顺利,希望如此,但假设您要组合箭头a~(f)~>b和b~(g)~>c。这实际上是Haskell函数a-(f)->mb和b-(g)->mc,您无法组合它们,因为类型不匹配。数学解决方案是使用单子的“乘法”自然变换u:mm->m,如下所示:a~(f)~>b~(g)~>c == a-(f)->mb-(mg)->mmc-(u_c)->mc,以获得一个a->mc的箭头,它是所需的Kleisli箭头a~(f;g)~>c。
也许这里的一个具体例子有所帮助。在Maybe单子中,您无法直接组合函数f:a->Maybe b和g:b->Maybe c,但通过将g提升至
Maybe_g :: Maybe b -> Maybe (Maybe c)
Maybe_g Nothing = Nothing
Maybe_g (Just a) = Just (g a)
并且使用“显而易见”的方法
u :: Maybe (Maybe c) -> Maybe c
u Nothing = Nothing
u (Just Nothing) = Nothing
u (Just (Just c)) = Just c
你可以形成组合 u . Maybe_g . f
,这是您想要的函数a -> Maybe c。
在(State s) monad中,它类似但比较杂乱:给定两个单子函数 a ~(f)~> b 和 b ~(g)~> c,它们实际上是在内部进行了适当的转换成 a -(f)-> (s->(s,b)) 和 b -(g)-> (s->(s,c)),您可以将它们组合起来通过将 g 提升(lift)到
State_s_g :: (s->(s,b)) -> (s->(s,(s->(s,c))))
State_s_g p s1 = let (s2, b) = p s1 in (s2, g b)
然后您将应用“乘法”自然变换u,即
u :: (s->(s,(s->(s,c)))) -> (s->(s,c))
u p1 s1 = let (s2, p2) = p1 s1 in p2 s2
这个函数有点像把 f
的最终状态插入到 g
的初始状态中。
Haskell 中,这种方式有些不自然,因此有 (>>=)
函数,它基本上与 u 做的事情相同,但更容易实现和使用。重要的是: (>>=)
不是自然变换 'u'。你可以用一个定义来表示另一个,所以它们是等价的,但它们不是同一件事。 Haskell 版本的 'u' 被写成 join
。
Kleisli 类别定义中还缺少每个对象的恒等性:一个 ~(1_a)~> a,它其实是一个 -(n_a)-> ma,其中 n 是“单位”自然变换。在 Haskell 中,这被写作 return
,并且似乎不会引起太多混淆。
在我学习 Haskell 之前,我学过范畴论,我也遇到了数学家所说的单子与在 Haskell 中的样子之间的不匹配问题。从另一个方向来看,这也并不容易!
m
的域和陪域(它们是范畴)应该相同,这里是 Haskell 类型的范畴。 - Alexandre C.fmap
所要求的类型签名不够一般化,无法描述所有在Hask上的自函子,只能描述所有从Hask到某个类型构造子定义的子范畴上的函子。 - C. A. McCann