为什么Haskell的`Functor`实例没有定义一个类似“return”的函数?

4
在范畴论中,一个函子是指两个范畴之间的映射,即将范畴 A 中的每个对象映射到另一个范畴 B 中的对象,并将每个态射 C -> D 映射到 B 中的相应对象,同时保持态射的组合。因此,可以说函子由两个“部分”组成,一个将对象映射到对象,另一个将态射映射到态射。
在 Haskell 中,如果我理解正确的话,在 Functor 类型中的每个类型都可以被“映射”,即类型为 a -> b 的函数可以被映射到函数 F a -> F b 中。那么为什么不存在一个 return :: Functor f => a -> f a 函数,特别是针对 Functor?是因为没有必要吗,因为我们可以简单地利用 Monad 实例中的 return 定义,因为 return 实际上只对一个 Monad 的函子部分起作用,还是有其他原因呢?

这让我感到奇怪,因为如果是这样的话,为什么return不包含在Functor实例中呢?我的意思是每个单子都有一个函子部分,所以对我来说,那是有道理的。有人能在这方面给我启示吗?


3
在范畴论中,一般情况下函子不允许对于所有的 a 都有 return::a->F a。例如,在集合范畴中,定义 F a = emptySet(并且 F anyMorphism = identity_emptySet)可以构造一个函子,但是对于非空的 a,不存在 a -> F a - chi
1
实际上,在data.pointed中有一个Pointed类型类,其中包含point :: a -> p a。但并非每个函子都可以成为Pointed的实例,例如data Void a deriving Functor(基本上是由@chi给出的原因)。 - sigfpe
请查看 https://dev59.com/B-k5XIcBkEYKwwoY2NPu#67192985,了解“函子”类的另一种实现方式,其中“对象到对象”的映射明确显示。 (它与“返回”非常不同。) - leftaroundabout
在 Haskell 中,类型类解析是类型导向的,因此 Functor f 实例的结构由 f 确定。f 本身是对象映射(映射 Type -> Type),而 fmap @f 是箭头映射(将 a -> a' 映射到 f a -> f a')。这些是两种映射方式,其中对象映射确定了箭头映射。 - Iceland_jack
1个回答

9

这是一个常见的误解,曾经也让我困惑过。你说得对,一个函数对象有两个部分:一个将对象映射到对象的部分,以及一个将函数映射到函数的部分(更一般地,将箭头映射到箭头,但这在这里并不重要)。现在,当我有

instance Functor F where
    fmap f x = ...

你已经正确推断出 fmap 接受函数作为输入并生成函数作为输出。然而,我们已经有一种方法将对象转换为其他对象了。从范畴论的角度来看,我们的对象不是值,而是类型。因此,“对象到对象”的部分应该将一个类型映射到另一个类型。这个东西被称为 F。我们函子的名称实际上就是从对象到对象的映射。给定一个类型 a,通过我们的函子提升后得到的类型 F a 是另一个类型。
现在这引出了一个公正的问题:什么是 return?它接受一个 a 并生成一个 F a(对于一个单子 F)。更具体地说,给定一个固定的单子 F,其签名为
return :: forall a. a -> F a

现在仔细阅读一下。它说“给定任何类型a,我都可以想出一个从a到Fa的函数”。也就是说,它接受我们类别中的一个对象(一种类型)并将其映射到我们类别中的一个箭头(一个函数)。从对象到箭头的映射称为natural transformation,这正是return所做的。
从范畴论的角度来看,一个单子是一个函子(基础类型F加上fmap),以及两个自然变换:
  • return,它是一个自然变换1 -> F(其中1是恒等函子),以及
  • join,它是一个自然变换F^2 -> F(其中F^2F与自身组合)
即对于任何类型 areturn 的类型是 a -> F ajoin 的类型是 F (F a) -> F a[1]
一个具有 returnfmap 的类型类会是什么样子呢?我不确定。我不知道是否有一个 Haskell 库实现了这个类型类,也不完全确定它的定律会是什么样子。一个好的猜测可能是 fmap f . return === return . f,但我们实际上可以把它作为一个 自由定理 得出,所以这不是我们的定律。如果你或其他人知道在 Hackage 生态系统中有这个类型类,请告诉我。

[1] Haskell使用绑定运算符(>>=)而不是join来等效定义"monad"。在数学上它们是等效的,我选择了更简单的定义方式。


1
给定任何类型 areturn 会生成一个类型为 a -> f a 的函数。通常我们只认为 return 是从 a -> f a 的函数,但这是因为 Haskell 在前面隐藏了 forall a。但是有一个隐藏的类型参数。如果你熟悉 TypeApplications,你可以使类型参数明确化。例如(在特定情况下的 Monad Maybe),return 是一个多态函数,但 return @Int 具有类型 Int -> Maybe Int,是一个单一的具体函数。 - Silvio Mayolo
1
没错!这正是范畴论中的自然变换。当您指定一个类型时,它会生成一个处理该类型的函数。 - Silvio Mayolo
2
@Ben,注意链接的参考文献从未涉及functors之间的映射。如果F和G是从范畴C到D的两个functors,则从“F到G”的自然变换不是从F到G的映射。相反,它是一组由C中的对象索引的箭头集合。对于C中的每个x,它在范畴D中给出一个箭头F(x)->G(x)。因此,它可以被视为从C中的对象到D中的箭头的映射。或者,在Haskell中,它可以被视为从类型(Hask中的对象)到函数(Hask中的箭头)的映射,将每个类型a映射到具有签名F a -> G a的函数。 - K. A. Buhr
1
@Ben 是的,你说得对,在Haskell中,不是所有的箭头集合都适用。但是特别地,在Haskell中,参数化使得创建一组箭头同时具有必要的多态性并违反交换图表成为不可能。 - Daniel Wagner
2
@Ben实际上,有趣的是这基本上就是我在上面所说的“自由定理”的意思。在我链接的“自由定理”短文中,Wadler解释说,任何可以在具有参数性的类型系统中编写并接受类型参数的函数都是一个(松散的)自然变换。 - Silvio Mayolo
显示剩余7条评论

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