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

sch*_*ine 0 haskell tuples functor

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

instance Functor (,) where
    fmap f (a,b) = (a,f b)
Run Code Online (Sandbox Code Playgroud)

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

> fmap (const 5) [1, 2]
[5,5]
> fmap (const 5) (1, 2)
(1,5)
Run Code Online (Sandbox Code Playgroud)

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

instance Functor (,) where
    fmap f (a,b) = (f a,f b)
Run Code Online (Sandbox Code Playgroud)

它会像这样工作:

> fmap (const 5) (1, 2)
(5,5)
Run Code Online (Sandbox Code Playgroud)

为什么不是这样?

lef*_*out 5

实例实际上并不instance Functor (,)像你说的那样。那不会是好心的:

Prelude> :k Functor
Functor :: (* -> *) -> Constraint
Prelude> :k (,)
(,) :: * -> * -> *
Run Code Online (Sandbox Code Playgroud)

即,(,)需要两个类型参数(两个元组字段的类型)来构造一个元组类型,但Functor该类实际上是用于只接受一个参数的类型构造函数,例如

Prelude> :k []
[] :: * -> *
Prelude> :k IO
IO :: * -> *
Run Code Online (Sandbox Code Playgroud)

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

Prelude> :k (,) Int
(,) Int :: * -> *
Run Code Online (Sandbox Code Playgroud)

所以你可以例如有

instance Functor ((,) Int)
Run Code Online (Sandbox Code Playgroud)

显然它不依赖于左字段的具体类型,所以你也可以制作它

instance Functor ((,) a)
Run Code Online (Sandbox Code Playgroud)

在价值级别的 Haskell 中,这部分将被写成

instance Functor (a,)
Run Code Online (Sandbox Code Playgroud)

与术语级别不同,部分应用程序适用于最左边的参数(该部分(,b)实际上是 for 的糖\a -> (a,b),但 Haskell 中没有类型级别的 lambdas),因此这里instance Functor ((,) a)是唯一可能的实例。

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