虽然这里还有其他一些非常精彩的答案,但它们都有些遗漏了您的第一个问题。需要明确的是,值 根本不存在并且 在 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 Functors,Hask 中的 endofunctors,我们也必须小心。在 Hask 中,对象是 Haskell types,箭头是这些类型之间的 Haskell functions。
让我们再看看Maybe。如果它将成为 Hask 上的 endofunctor,那么我们需要一种方法将 Hask 中的所有 types 转换为 Hask 中的其他 types。这个映射不是一个 Haskell 函数,尽管它看起来像一个:pure :: a -> Maybe a 不符合条件,因为它在 value 级别上运行。相反,我们的对象映射是Maybe 本身:对于任何类型a,我们都可以形成Maybe a 类型。
这已经凸显了在没有价值的情况下使用 Hask 工作的价值——我们确实想要隔离一个不依赖于 pure 的 Functor 概念。
我们将通过检查我们的Maybe endofunctor 的箭头映射来开发Functor 的其余部分。这里我们需要一种将 Hask 的箭头映射到 Hask 的箭头的方法。现在让我们假设这不是 Haskell 函数——它不一定是——所以为了强调它,我们将用不同的方式编写它。如果 f 是 Haskell 函数 a -> b 那么 Maybe[f] 是其他 Haskell 函数 Maybe a -> Maybe b。
现在,很难不跳过并开始调用 Maybe[f] "fmap f",但在跳转之前我们可以做更多的工作。也许[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] 。也许[g]。
最后一步是注意到 Maybe[_] 恰好可以作为 Haskell 函数本身作为 Hask 对象 forall a b . (a -> b) -> (Maybe a -> Maybe b) 的值来实现。这给了我们Functor。
虽然上述内容相当技术性和复杂性,但重要的一点是保持 Hask 和分类内函子的概念直截了当,并与它们的 Haskell 实例化分开。特别是,我们可以发现所有这些结构,而无需引入 fmap 作为真正的 Haskell 函数存在的需要。 Hask 是一个在价值层面根本没有引入任何东西的类别。
这就是将 Hask 视为一个类别的真正核心所在。在 Hask 上用Functor 标识内函子的符号需要更多的线条模糊。
这种线条模糊是合理的,因为Hask 具有指数。这是一种棘手的说法,即整束分类箭头与 Hask 中特定的特殊对象之间存在统一。
更明确地说,我们知道对于 Hask 的任何两个对象,比如 a 和 b,我们可以讨论这两个对象之间的箭头,通常表示为 Hask(a, b )。这只是一个数学集合,但我们知道 Hask 中还有另一种类型与 Hask 密切相关(a,b):(a -> b)!
所以这很奇怪。
我最初声明一般的 Haskell 值在 Hask 的分类表示中绝对没有代表。然后我继续证明我们可以通过仅使用它的分类概念而不实际将这些部分作为值粘贴到 Haskell 中来对 Hask 做很多事情。
但现在我注意到像 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 的箭头(@ 987654381@, a) 对于每个 Hask 对象 a。
从哲学上讲,我们可能会争辩说这些箭头真的不是我们正在寻找的 Haskell 值——它们只是替身,分类模拟。你可能会争辩说它们是不同的东西,只是碰巧与 Haskell 值一一对应。
我实际上认为这是一个非常重要的想法。这两件事是不同的,它们只是行为相似。
非常相似。任何类别都可以让您组合箭头,因此假设我们在 Hask 中选择了一些箭头(a,b),在 Hask 中选择了一些箭头((),a)。如果我们将这些箭头与类别组合结合起来,我们会在 Hask 中得到一个箭头((), b)。稍微把这一切颠倒过来,我们可能会说我刚才所做的是找到一个a -> b 类型的值,一个a 类型的值,然后将它们组合起来产生一个b 类型的值。
换句话说,如果我们从侧面看事物,我们可以将分类箭头组合视为函数应用的一种通用形式。
这就是让像 Hask 这样的类别如此有趣的原因。从广义上讲,这些类别称为笛卡尔封闭类别或 CCC。由于同时具有初始对象和指数(也需要乘积),它们具有完全模拟类型化 lambda 演算的结构。
但它们仍然没有值。
[1] 如果您在阅读我的其余答案之前阅读此内容,请继续阅读。事实证明,虽然期望会发生这种情况是荒谬的,但它确实会发生。如果您在阅读我的全部答案后阅读此内容,那么让我们来思考一下 CCC 有多酷。