虽然这里还有其他一些非常棒的答案,但它们都有些想念你的第一个问题。需要明确的是,值在 Hask 类别中根本不存在并且没有任何意义。这不是 Hask 要谈论的。
上面说起来或感觉起来似乎有点愚蠢,但我提出来是因为重要的是要注意范畴论只提供了一个镜头来检查像编程语言这样复杂的东西中可用的更复杂的交互和结构。期望所有这些结构都包含在一个相当简单的类别概念中是没有成果的。[1]
另一种说法是,我们正在尝试分析一个复杂的系统,有时将其视为一个类别以寻找有趣的模式很有用。正是这种心态让我们引入了 Hask,检查它是否真的形成了一个类别,注意它Maybe
的行为似乎像一个 Functor,然后使用所有这些机制来写下一致性条件。
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 中的 endofunctors,我们也必须小心。在 Hask 中,对象是 Haskell类型,箭头是这些类型之间的 Haskell函数。
让我们再看看Maybe
。如果它将成为 Hask 上的 endofunctor,那么我们需要一种方法将 Hask 中的所有类型转换为 Hask 中的其他类型。这个映射不是一个 Haskell 函数,尽管它可能感觉像一个:pure :: a -> Maybe a
不符合条件,因为它在值级别上运行。相反,我们的对象映射Maybe
本身就是:对于任何类型a
,我们都可以形成 type Maybe a
。
这已经突出了在没有值的情况下在 Hask 中工作的价值——我们确实想要隔离一个Functor
不依赖于pure
.
我们将Functor
通过检查我们的内函子的箭头映射来开发其余部分Maybe
。这里我们需要一种将 Hask 的箭头映射到 Hask 的箭头的方法。现在让我们假设这不是一个 Haskell 函数——它不一定是——所以为了强调它,我们将用不同的方式编写它。如果f
是 Haskell 函数a -> b
,那么 Maybe[ f
] 是其他一些 Haskell 函数Maybe a -> Maybe b
。
现在,很难不跳过并开始调用 Maybe[ f
] " fmap f
",但是在跳转之前我们可以做更多的工作。Maybe[ f
] 需要有一定的连贯条件。特别是,对于a
Hask 中的任何类型,我们都有一个 id 箭头。在我们的元语言中,我们可能称它为 id[ a
],而且我们碰巧知道它也使用 Haskell 名称id :: a -> a
。总之,我们可以使用这些来说明内函子相干条件:
对于 Hask 中的所有对象a
,我们有 Maybe[id[ a
]] = id[ Maybe a
]。对于 Haskf
和中的任意两个箭头g
,我们有 Maybe[ f . g
] = Maybe[ f
] 。也许[ g
]。
最后一步是注意到 Maybe[_] 恰好可以作为 Haskell 函数本身作为 Hask 对象的值来实现forall a b . (a -> b) -> (Maybe a -> Maybe b)
。这给了我们Functor
。
虽然上述内容相当技术性和复杂性,但重要的一点是保持 Hask 和分类内函子的概念直截了当,并与它们的 Haskell 实例化分开。特别是,我们可以发现所有这些结构,而无需引入fmap
作为真正的 Haskell 函数存在的需要。Hask 是一个在价值层面根本没有引入任何东西的类别。
这就是将 Hask 视为一个类别的真正核心所在。在 Hask 上识别 endofunctors 的符号Functor
需要更多的线模糊。
这种线模糊是合理的,因为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 对象,我们就会知道在 Hask( , )a
中有一整套分类箭头。此外,我们知道这些对应于 type 的值。最后,由于我们知道给定任何函数,我们可以通过应用立即得到一个,因此可能想说 Hask( , ) 中的分类箭头正是Haskell类型的值。()
a
() -> a
f :: () -> a
a
()
()
a
a
这应该要么完全令人困惑,要么令人难以置信。
我将坚持我最初的陈述,从哲学上结束这一点:Hask 根本不谈论 Haskell 价值观。它确实不是一个纯粹的类别——类别之所以有趣,正是因为它们非常简单,因此不需要所有这些额外的类型、值和typeOf
包含等概念。
但我也(也许很糟糕)表明,即使作为一个严格的类别,Hask 也有一些看起来非常非常类似于 Haskell 的所有值的东西:每个 Hask 对象的 Hask( ()
, ) 的箭头。a
a
从哲学上讲,我们可能会争辩说这些箭头并不是我们正在寻找的真正的 Haskell 值——它们只是替身,分类模拟。你可能会争辩说它们是不同的东西,只是碰巧与 Haskell 值一一对应。
我实际上认为这是一个非常重要的想法。这两件事是不同的,它们只是行为相似。
非常相似。任何类别都可以让您组合箭头,因此假设我们在 Hask( , ) 中选择了一些箭头,在 Hask( , a
)b
中选择了一些箭头。如果我们将这些箭头与类别组合结合起来,我们会在 Hask( , ) 中得到一个箭头。稍微把这一切放在头上,我们可能会说我刚刚做的是找到一个 type的值,一个 type 的值,然后将它们组合起来产生一个 type 的值。()
a
()
b
a -> b
a
b
换句话说,如果我们从侧面看事物,我们可以将分类箭头组合视为函数应用的一种通用形式。
这就是让像 Hask 这样的类别如此有趣的原因。从广义上讲,这些类别称为笛卡尔封闭类别或 CCC。由于同时具有初始对象和指数(也需要乘积),它们具有完全模拟类型化 lambda 演算的结构。
但他们仍然没有价值。
[1] 如果您在阅读我的其余答案之前阅读此内容,请继续阅读。事实证明,虽然期望这种情况发生是荒谬的,但它确实确实发生了。如果您在阅读了我的全部答案后正在阅读本文,那么让我们反思一下 CCC 有多酷。