在范畴论的术语中,什么是函数式编程中的单子?

54
每次有人承诺“解释单子”时,我的兴趣就被激发了,但当所谓的“解释”是一长串示例列表,并以一些轻描淡写的话结束, 说这些“高深思想”背后的“数学理论”在“这个阶段太复杂了,无法解释”时,我的兴趣就变成了沮丧。
现在我要求相反的情况。我对范畴论有着扎实的掌握,并不害怕追逐图表、Yoneda引理或导出函子(事实上也知道单子和范畴意义下的伴随)。
有人能给我提供一个在函数式编程中单子清晰简明的定义吗?例子越少越好:有时一个清晰的概念比一百个腼腆的例子更有意义。Haskell可以作为演示语言,但我不挑剔。

2
也许是这个?http://en.wikipedia.org/wiki/Monad_%28category_theory%29#Formal_definition - kennytm
6
在Haskell中,单子是指在Haskell类型和函数的范畴中的单子(重复某人在#haskell IRC频道告诉我的话)。http://www.haskell.org/haskellwiki/Category_theory的介绍很有深度。 - Joey Adams
5
不要脸地自夸一下:也许你会喜欢我的零比喻Monad教程 - Dan Burton
投反对票的人,能否解释一下你的反对理由? - Kerrek SB
1
@Hjulle 内容已经移动至:https://unknownparallel.wordpress.com/zero-analogy-monad-tutorial/ - toraritte
显示剩余6条评论
8个回答

32

这个问题有一些很好的答案:单子作为伴随

更重要的是,Derek Elkins的"TMR#13中用范畴论计算单子"文章应该有您寻找的构造:http://www.haskell.org/wikiupload/8/85/TMR-Issue13.pdf

最后,也许这真的是您要寻找的最接近的内容,您可以直接查看Moggi在1988-91年对该主题的开创性论文:http://www.disi.unige.it/person/MoggiE/publications.html

特别是请参见“计算和单子的概念”。


我的自己的简短而不够精确的观点:

从一个类别Hask开始,其对象是Haskell类型,其态射是函数。函数也是Hask中的对象,乘积也是如此。因此,Hask是笛卡尔闭合的。现在引入一个箭头,将Hask中的每个对象映射到MHask中,后者是Hask对象的子集。单位!接下来,引入一个箭头,将Hask上的每个箭头映射到MHask上的箭头。这给了我们映射,并使MHask成为协变的自函子。现在引入一个箭头,将从MHask中生成的每个MHask对象(通过单位)映射到生成它的MHask对象。连接!从那里开始,MHask就是一个单子(更精确地说是一个单调的自函子)。

我相信上面的内容存在不足之处,这也是为什么如果你在寻求形式主义方面的知识,我真的建议你特别参考Moggi论文。


1
这是目前为止唯一一个真正回答了问题的评论。 - John F. Miller
1
只有一个小问题:Hask 的对象是 Haskell 中的类型,而不是值。箭头表示 Haskell 函数的定义域和值域分别为相应的类型。 - Lambdageek
@Lambdageek:好的,已修复。 - sclv
1
希望这样做没问题,我接受了其他答案; 我想你已经在这个问题上获得了大量声誉,这样每个人都会有一点奖励。非常感谢您的回答! - Kerrek SB

19
作为对Carl答案的补充,Haskell中的Monad(理论上)是这样定义的:
class Monad m where
  join :: m (m a) -> m a
  return :: a -> m a
  fmap :: (a -> b) -> m a -> m b

请注意,"bind"(>>=)可以被定义为
x >>= f = join (fmap f x)

根据Haskell Wiki,在范畴C中,单子是一个三元组(F:C→C,η:Id→F,μ:F∘F→F),带有一些公理。对于Haskell,fmap、return和join分别与F、η和μ对应(在Haskell中,fmap定义了Functor)。如果我没记错的话,Scala分别称其为map、pure和join(Scala将bind称为flatMap)。

8
我喜欢这个答案,因为尽管单子(monads)对我们作为程序员来说主要因为 >>=有用,但是 (fmap, join, return) 这三个操作更能阐明单子(imo)的本质。当我最终理解了"一个单子只不过是范畴内自函子构成的幺半群"实际上意味着什么时,我受到了启发。 - dave4420

12

好的,使用 Haskell 的术语和示例...

在函数式编程中,单子是一种用于具有类型 *-> * 的数据类型的组合模式。

class Monad (m :: * -> *) where
    return :: a -> m a
    (>>=)  :: m a -> (a -> m b) -> m b

(在Haskell中,类别还有更多内容,但那些是重要的部分。)

如果一个数据类型能够实现monad接口并满足三个实现条件,那么它就是一个monad。这些条件被称为“monad定律”,具体解释可以参考较长的说明。我将这些法则总结为"(>>= return)是一个恒等函数,(>>=)是可结合的"。即使可以更加精确地表达,但其实不会比这更复杂了。

而这就是一个monad的全部内容。只要你能够保持这些行为属性,同时实现该接口,那么你就拥有了一个monad。

这个解释可能比你预期的要简短。因为monad接口的抽象程度非常高。抽象的程度是为什么如此多的不同事物都可以被建模为monad的一部分原因。

更不容易理解的是,尽管接口如此抽象,但它允许通用建模任何控制流模式,无论实际的monad实现如何。这就是为什么GHC的base库中的Control.Monad包具有像whenforever等组合子的原因。这也是为什么能够明确地对任何monad实现进行抽象是强大的,特别是在类型系统的支持下。


你忘记了fmap!你需要fmap - sclv
4
不, fmap也可以用>>=return来定义:fmap f m = m >>= return . f - hammar

6

5

一个单子(Monad)是在自函子范畴中的幺半群,这有什么问题呢?.

开玩笑的,就我个人而言,我认为单子在Haskell和函数编程中的使用更好地从“单子作为接口”(如Carl和Dan的回答所示)而非“单子作为范畴论术语”的角度解释。不得不承认,当我在一个真实项目中使用另一种语言的monadic时,我才真正理解了整个单子概念。

你提到你不喜欢所有“大量示例”的教程。是否有人向你介绍过Akward squad 论文?它主要关注IO monad,但其介绍给我们一个好的技术和历史背景,说明Haskell最初为什么拥抱单子概念。


我很欣赏你的第一句话(一个简明定义的好例子),但也许我的问题没有表达清楚:我非常喜欢范畴单子的概念。我想了解函数式编程中单子是如何定义的,用范畴术语来解释。或者实际上,用任何你喜欢的术语来解释,只要它清晰、精确、简洁。如果使用范畴术语最容易解释,那就用它吧 :-) 如果你认为仅用编程术语来解释更好,也可以试试。 - Kerrek SB
抱歉,那个解释实际上是一个笑话(:/)。至于简洁性,我认为试图将单子归纳为一句话很难 - 你可以给出精确的接口定义(正如其他人在这个帖子中已经给出的),但是你会错过为什么要首先通过这种麻烦来解释的解释。不知道,我是那种只有在你能说服我它们首先存在的必要性时才能理解事物的人。 - hugomg

4
我不太确定自己在说什么,但是这是我的看法:
单子(Monads)用于表示计算。你可以将一个普通的过程式程序,它基本上是一系列语句的列表,视为一些组成计算的组合。单子是这个概念的一般化,允许你定义语句如何被组合。每个计算都有一个值(它可能只是());单子只是决定值如何通过一系列计算进行操作的。
Do 表示法(Do notation)真正让这一点清晰:它基本上是一种特殊的基于语句的语言,让你定义语句之间发生了什么。就好像你可以定义类似C语言中";"的工作方式一样。
从这个角度来看,我使用过的所有单子都有意义:State 不影响值,但更新一个第二值,该值在后台从计算到计算传递;如果Maybe 遇到 Nothing,则短路计算;List 允许你通过传递变量数量的值;IO 让你以安全的方式传递不纯的值。我使用过的更专业化的单子,如 Gen 和 Parsec 解析器也是类似的。
希望这是一个清晰的解释,没有完全偏离主题。

4

由于您理解范畴论意义下的单子,我将解释您的问题与函数式编程中单子的表达方式有关。 因此,我的答案避免了任何有关单子是什么或其含义或用途的解释。

答案:在Haskell中,单子以某个范畴的内部语言的Kleisli三元组的(内部化)映射的形式呈现。

解释:很难精确描述“Hask”类别的属性,并且这些属性对于理解Haskell的单子表示方法基本上没有影响。 因此,对于这个讨论,更有用的是将Haskell视为某个类别C内部语言。 Haskell函数定义了C中的态射,而Haskell类型是C中的对象,但是进行这些定义的特定类别并不重要。

参数化数据类型,例如data F a = ...,是对象映射,例如F:|C| -> |C|

在Haskell中,单子的通常描述是以Kleisli triple(或Kleisli extension)形式呈现:

class Monad m where
    return :: a -> m a
    (>>=) :: m a -> (a -> m b) -> m b

其中:

  • m是对象映射m :|C| -> |C|
  • return是对象上的单元操作
  • >>=(Haskeller称之为"bind")是态射上的扩展操作,但其前两个参数已经交换了位置(与扩展的通常签名(-)* : (a -> m b) -> m a -> m b相比)

(这些映射本身作为C中的态射家族进行内部化,这是可能的,因为m :|C| -> |C|)。

因此,如果你已经接触过Haskell的do记法,它就是Kleisli范畴的内部语言。


2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接