尽管这里还有其他非常出色的答案,但它们都在某种程度上忽略了您的第一个问题。要明确的是,在Hask范畴中,值
根本不存在,并且
没有意义。这不是Hask用来讨论的内容。
以上观点似乎有点愚蠢或感性,但我提出来是因为重要的是要注意,范畴论只提供了一种检查编程语言中复杂交互和结构的透镜。期望所有这些结构都被包含在范畴的相当简单的概念中是不可行的。[1]
换句话说,我们正在尝试分析一个复杂的系统,有时候将其视为一个范畴是有用的,以便寻找有趣的模式。正是这种思维方式让我们引入了Hask,检查它是否真的形成一个范畴,注意到Maybe似乎像一个函子,然后使用所有这些机制来写下一致性条件。
fmap id = id
fmap f . fmap g = fmap (f . g)
这些规则不管我们是否引入Hask,都是有意义的。但通过将它们视为在Haskell中发现的简单结构的简单后果,我们可以理解它们的重要性。
作为技术注释,本答案的全部内容都假设Hask实际上是"柏拉图式"的Hask,也就是说,我们可以尽可能地忽略底部(undefined
和非终止)。如果没有这个假设,几乎整个论点都会瓦解一点。
让我们仔细检查这些法则,因为它们似乎几乎与我的最初陈述相违背——它们明显在值级别上操作,但是“值在Hask中不存在”,对吧?
那么,一个回答是更仔细地看一下范畴函子是什么。明确地说,它是两个范畴(比如C和D)之间的映射,它将C中的对象映射到D中的对象,C中的箭头映射到D中的箭头。值得注意的是,通常这些“映射”不是范畴箭头——它们只是形成类别之间的关系,不一定与类别共享结构。
这很重要,因为即使考虑了Haskell的Functor
s,也必须小心。在Hask中,对象是Haskell类型,箭头是在那些类型之间的Haskell函数。
让我们再看一下Maybe
。如果它要成为Hask上的一个自函子,那么我们需要一种方法来将Hask中的所有类型映射到其他类型。这个映射不是一个Haskell函数,即使它可能感觉像一个: pure :: a -> Maybe a
并不符合要求,因为它在值级别上操作。相反,我们的对象映射是Maybe
本身:对于任何类型a
,我们都可以形成类型Maybe a
。
这已经突出了在没有值的情况下在Hask中工作的价值——我们确实希望隔离一个不依赖于pure
的Functor
概念。
我们将通过检查我们的Maybe
自函子的箭头映射来开发Functor
的其余部分。在这里,我们需要一种将Hask的箭头映射到Hask的箭头的方法。让我们暂时假设这不是一个Haskell函数——它不必是——所以为了强调它,我们将用不同的方式写它。如果f
是一个Haskell函数a -> b
,那么Maybe[f
]是另一个Haskell函数Maybe a -> Maybe b
。
现在,很难不直接称呼Maybe [
f
]为“
fmap f
”,但在跳入这一步之前,我们可以再做些工作。Maybe [
f
]需要具备某些相容性条件。特别地,在Hask中的任何类型
a
都有一个id箭头。在我们的元语言中,我们可能称之为id [
a
],我们也知道它还叫做Haskell名称
id :: a -> a
。总之,我们可以使用这些来陈述范畴自函子的相容性条件:
对于Hask中的所有对象
a
,我们有Maybe[id[
a
]] = id[
Maybe a
]。对于Hask中的任意两个箭头
f
和
g
,我们有Maybe[
f . g
]] = Maybe[
f
]. Maybe[
g
]。
最后一步是注意到Maybe [_]恰好可以作为一个Hask对象的值本身作为Haskell函数实现,该对象的类型为
forall a b . (a -> b) -> (Maybe a -> Maybe b)
。这给了我们
Functor
。
虽然上述内容相当技术性和复杂,但保持概念清晰并将Hask和范畴自函子的概念与它们的Haskell实例化分开,这一点非常重要。特别地,我们可以发现所有这些结构,而无需引入需要fmap作为真正的Haskell函数存在的需要。Hask是一个在值级别上不需要引入任何东西的范畴。
这就是将Hask视为范畴的真正核心所在。用
Functor
来标识Hask上的自函子需要更多的线条模糊。
这种线条模糊是有道理的,因为Hask具有指数。这是一种棘手的方式,即范畴箭头的整个捆绑和Hask中特定的、特殊的对象之间存在统一。
更明确地说,我们知道对于Hask的任意两个对象,比如
a
和
b
,我们可以谈论连接这两个对象的箭头,通常表示为Hask(
a
,
b
)。这只是一个数学集合,但我们知道Hask中还有另一种类型与Hask(
a
,
b
)密切相关:
(a -> b)
!
这很奇怪。
我最初声明一般的Haskell值在Hask的范畴呈现中根本没有表示。然后我接着演示了我们可以仅使用其范畴概念就可以做很多Hask的工作,而不必将这些部分实际放入Haskell中作为值。
现在我注意到像a -> b
这样的类型的值实际上存在于元语言集合Hask (a
, b
)中的所有箭头中。这是一个很棒的技巧,正是这种元语言的模糊使得具有指数的范畴变得如此有趣。
不过,我们可以做得更好!Hask还有一个终端对象。我们可以通过元语言方式称之为0,但我们也知道它是Haskell类型()
。如果我们查看任何Hask对象a
,我们知道有一整套范畴箭头在Hask(()
,a
)中。进一步地,我们知道它们对应于类型() -> a
的值。最后,因为我们知道给定任何函数f :: () -> a
,我们可以通过应用()
立即获得a
,所以我们可能想说,在Hask(()
,a
)中的范畴箭头恰好是类型a
的Haskell值。
这可能会非常令人困惑或者非常令人震惊。
我将以某种哲学的方式结束这个话题:Hask根本不讨论Haskell值。作为一个纯范畴,它实际上并没有讨论这些额外的类型、值和typeOf
包含等范畴外概念,正是因为范畴非常简单,所以才有趣。
但我还是展示了即使作为一个严格的仅仅是一个范畴,Hask也具有类似于所有Haskell值的东西:对于每个Hask对象a
,Hask(()
,a
)的箭头看起来非常相似。
从哲学上讲,我们可以认为这些箭头并不是我们正在寻找的Haskell值---它们只是替代品,范畴模拟。你可能会说它们是不同的东西,只是碰巧与Haskell值一一对应。
我认为这是一个非常重要的想法。这两件事情是不同的,但它们的行为非常相似。
非常相似。任何范畴都可以组合箭头,所以假设我们选择Hask(a
,b
)中的一些箭头和Hask(()
,a
)中的一些箭头。如果我们将这些箭头与范畴组合结合,我们得到Hask(()
,b
)中的一个箭头。稍微转变一下思路,我们可以说,我刚才找到了类型a -> b
的值,类型a
的值,然后将它们组合起来产生了类型b
的值。
换句话说,如果我们侧面看事物,我们可以将范畴箭头组合看作函数应用的一般形式。
这就是使Hask等类别如此有趣的原因。广义上,这些类型的类别被称为笛卡尔闭类别或CCCs。由于拥有初始对象和指数(也需要乘积),它们具有完全模拟类型λ演算的结构。
但它们仍然没有值。
[1] 如果您在阅读我的答案之前阅读本文,则请继续阅读。事实证明,虽然期望发生这种情况是荒谬的,但实际上确实有点像。如果您是在阅读完整个答案后再阅读本文,则让我们反思一下CCC有多酷。