为什么 (,) 的 Functor 实例映射到第二个值?

Why is the Functor instance for (,) mapping on the second value?

在Haskell中,(,)的Functor实例显然是

instance Functor (,) where
    fmap f (a,b) = (a,f b)

这导致了一个不直观的事实:

> fmap (const 5) [1, 2]
[5,5]
> fmap (const 5) (1, 2)
(1,5)

现在,在我看来,使用这个定义会更好:

instance Functor (,) where
    fmap f (a,b) = (f a,f b)

它会像这样工作:

> fmap (const 5) (1, 2)
(5,5)

为什么不是这样?

此行为可从 Product 仿函数获得:

Data.Functor.Product Data.Functor.Identity> fmap (+1) (Pair 5 6) :: Product Identity Identity Integer
Pair (Identity 6) (Identity 7)

如您所见,Product(,) 之间的主要区别在于 Pair 的两个元素具有相同的叶类型(在本例中,Integer). (相关:请注意,在上面的类型中,Integer 仅被提及一次,而在 (5,6) 的类型中,Integer 被提及 两次 。 ) 由于 (,) 可能包含完全不相关的类型,因此不能保证您可以以类型正确的方式将单个函数应用于两个部分。

实例其实不是你说的instance Functor (,)。那可不是好心人:

Prelude> :k Functor
Functor :: (* -> *) -> Constraint
Prelude> :k (,)
(,) :: * -> * -> *

(,) 采用两个类型参数(两个元组字段的类型)来构造元组类型,但 Functor class 实际上用于类型构造函数只需要一个参数,比如

Prelude> :k []
[] :: * -> *
Prelude> :k IO
IO :: * -> *

那么为什么会有一个仿函数实例呢?坦率地说,我认为这是应该 not 定义的实例之一,正是因为元组不对称令人困惑。然而,它实际上可以定义,而且只有一种方法可以做到,所以标准库的这种选择当然不是不合理。诀窍是柯里化:您可以将 (,) 构造函数部分应用于任何固定的左字段类型,然后为您提供一个单参数类型构造函数。例如

Prelude> :k (,) Int
(,) Int :: * -> *

所以你可以有

instance Functor ((,) Int)

显然它不依赖于左侧字段的具体类型,所以你也可以这样做

instance Functor ((,) a)

在价值层面Haskell,这部分会写成

instance Functor (a,)

与术语级别不同,部分应用 对最左边的参数起作用((,b) 部分实际上是 \a -> (a,b) 的糖分,但是 Haskell 中没有类型级别的 lambda,所以 instance Functor ((,) a) 是这里唯一可能的实例。

要获得您要求的行为,即应用于 both/all 字段的函数,您需要两个字段实际上具有相同的类型。即,您需要一个只有一个参数开头的类型构造函数,并且只需将该类型用于其 value 构造函数的字段两次。具有该行为的标准类型是 V2.