为什么 fmap 不适用于元组?

Why fmap doesn't work for a tuple?

在下面,我尝试在元组上使用 fmap,但这没有用,尽管它适用于列表和 Just 4:

Prelude> fmap (+3) (Just 4)
Just 7
Prelude> fmap (+3) [1,2,3,4]
[4,5,6,7]
Prelude> fmap (+3) (10,11,12,13,14)

<interactive>:38:1: error:
    * Non type-variable argument
        in the constraint: Functor ((,,,,) a b1 c d)
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        it :: forall a b1 c d b2.
              (Num d, Num c, Num b1, Num a, Num b2, Functor ((,,,,) a b1 c d)) =>
              (a, b1, c, d, b2)
Prelude>

元组是一组特定数量的值(可能)不同类型。即使考虑表达式 (10,11,12,13,14),每个元素也可能具有不同的类型:

Prelude> :t (10,11,12,13,14)
(10,11,12,13,14)
  :: (Num t, Num t1, Num t2, Num t3, Num t4) => (t4, t3, t2, t1, t)

例如,表达式 10 可能是 Int,而 11 可能是 Word,依此类推。

一般来说,你也可以写一个像("foo", 42, True, "bar", 11)这样的five-tuple,它有这样的类型:

Prelude> :t ("foo", 42, True, "bar", 11)
("foo", 42, True, "bar", 11)
  :: (Num t, Num t1) => ([Char], t1, Bool, [Char], t)

第一个元素是 [Char] 值(即 String),下一个元素是某个数字,第三个元素是 Bool,依此类推。

您不能将函数 +3 应用于所有这些元素。即使您认为它对字符串有意义(IMO,它不是),我希望您同意不能将 3 添加到 True.

由于它们的类型,元组通常不是 Functor 个实例,因此,您不能对它们使用 fmap


正如 Willem Van Onsem 在评论中指出的那样,two-tuple Functor,但它可能不会按照您预期的方式运行。 Haskell pair is a bit funky.

Why fmap doesn't work for a [5-]tuple?

因为还没有人将 5 元组 Functor 实例添加到 base。如果你看一下 the list of Functor instances provided by base,你会发现 Functor ((,) a),对的实例,但不是更大元组的实例,包括 Functor ((,,,,) a b c d),这是你在这里需要的。

后续问题当然是:为什么还没有人将 5 元组 Functor 实例添加到 base?一个原因与必要性(或缺乏必要性)有关:在实践中,对比较大的元组更频繁地出现,并且随着元组变得越来越大,越来越难以证明使用它们而不是 non-anonymous 类型是合理的适合一个人的用例。既然如此,对更大元组的 Functor 个实例的需求并不是那么大。

虽然您没有提及您期望 Functor ((,,,,) a b c d) 的行为,但值得注意的是 fmap 对仅作用于第二个组件,而较大元组的实例类似地只处理与最后一个组件。

GHCi> fmap not (False, True)
(False,False)

两个原因是:

  1. 组件的类型可以不同,没办法fmap not ("foobar", True) 可能会改变这两个组件。

  2. 在编写实例时无法翻转类型构造函数,例如对于作用于第一个组件的对,不能有 Functor 实例(除非您使用 a newtype wrapper,但这不是重点)。

虽然这种行为可能看起来令人惊讶,但如果您将类型为 (a, b) 的对视为带有注释的 b 值,这是完全合理的(标签,标签,额外的东西 - 但是你喜欢称它为 a 类型。在您宁愿将其视为可以独立修改的一对两个值的情况下,您可以求助于 Bifunctor class:

GHCi> import Data.Bifunctor
GHCi> first reverse ("foobar", True)
("raboof",True)
GHCi> second not ("foobar", True)
("foobar",False)
GHCi> bimap reverse not ("foobar", True)
("raboof",False)

base 不提供 TrifunctorTetrafunctor 等,因为不需要,如开头所述。)

在给更大的元组 Functor 实例时,以相同的方式对待它们同样合理;事实上,为了保持一致性,这些实例可以说应该存在。但是,有些人非常不喜欢配对实例,这导致 proposals to add the instances for other tuples 停滞不前。

P.S.: 也许值得一提的是 lens 库的(许多)用例之一正在使用 Functors 作为函子。这为我们提供了一种方便的方式来查看 Functor 和(如果那是一回事)Pentafunctor 实例将如何处理 5 元组:

GHCi> import Control.Lens
GHCi> over _5 (+3) (10,11,12,13,14)
(10,11,12,13,17)
GHCi> over _4 (+3) (10,11,12,13,14)
(10,11,12,16,14)
GHCi> over _3 (+3) (10,11,12,13,14)
(10,11,15,13,14)
GHCi> over _2 (+3) (10,11,12,13,14)
(10,14,12,13,14)
GHCi> over _1 (+3) (10,11,12,13,14)
(13,11,12,13,14)

甚至还有映射所有组件的方法...

GHCi> over both (+3) (13,14)
(16,17)
GHCi> over each (+3) (10,11,12,13,14)
(13,14,15,16,17)

...不过,毫不奇怪,他们要求所有组件都具有相同的类型:

GHCi> over each (+3) (True,11,12,13,14)

<interactive>:9:12: error:
    * No instance for (Num Bool) arising from a use of `+'
    * In the second argument of `over', namely `(+ 3)'
      In the expression: over each (+ 3) (True, 11, 12, 13, 14)
      In an equation for `it':
          it = over each (+ 3) (True, 11, 12, 13, 14)
GHCi> :set -XPartialTypeSignatures
GHCi> :set -fno-warn-partial-type-signatures
GHCi> :t \f -> over each f :: (_,_,_,_,_) -> _
\f -> over each f :: (_,_,_,_,_) -> _
  :: (w -> b5) -> (w, w, w, w, w) -> (b5, b5, b5, b5, b5)