`(fmap . fmap) sum Just [1, 2, 3]` 中的类型为什么能够工作?

8

我正在读非常好的《Haskell编程:从基础原理到实践》,感觉很兴奋。在电子书上的第1286页,我看到了以下例子,但我无法理解:

Prelude> (fmap . fmap) sum Just [1, 2, 3]
Just 6

我对以下内容的工作原理非常清楚:

这个问题显然很简单:

Prelude> fmap sum $ Just [1,2,3]
Just 6

我已经手动分解了(fmap . fmap)以理解类型的工作原理。但是,当将其视为“两次提升”时,这并没有意义,因为我正在对Just和List数据构造函数进行提升。

我在ghci中输入了以下内容:

Prelude> :t (fmap . fmap)
(fmap . fmap)
  :: (Functor f, Functor f1) => (a -> b) -> f1 (f a) -> f1 (f b)

Prelude> :t (fmap . fmap) sum
(fmap . fmap) sum
  :: (Num b, Foldable t, Functor f, Functor f1) =>
     f1 (f (t b)) -> f1 (f b)

Prelude> :t (fmap . fmap) sum Just
(fmap . fmap) sum Just :: (Num b, Foldable t) => t b -> Maybe b

我不理解如何得出最后的输出结果。当将(fmap . fmap) sum作为输入并使用Just数据构造函数时,编译器如何知道要替换f1f都为Maybe?在这里得到一个好答案之后,我应该如何自己找到答案呢?

3
扩展(fmap . fmap).的定义,然后简化。注意你对fmap的使用之一是将函数作为其第二个参数... - Carl
1
@FyodorSoikin,那个问题看起来几乎完全无关。 - dfeuer
被接受的答案解释了为什么以及如何运作。 - Fyodor Soikin
1
@FyodorSoikin 这里的问题看起来几乎是相反的: 我们有一个具有这种行为的函数,问题是它为什么有效。在链接的问题中,他们描述了这种行为,他们正在寻找一种实现。 - David Young
2个回答

9
这并不是针对MaybeList同时进行的提升操作(这会导致类型问题),而是针对函数类型(->)Maybe进行的操作。
Just :: a -> Maybe a
     -- ((->) a) (Maybe a)
     -- f (g a)   for f ~ ((->) a)  and  g ~ Maybe

(fmap . fmap) :: (a   -> b) -> f (g a  ) -> f (g b)
     -- Num x => ([x] -> x) -> f (g [x]) -> f (g x)
     -- Num x => ([x] -> x) -> ([x] -> Maybe [x]) -> [x] -> Maybe x
     --          ^             ^                     ^
     --          sum           Just                  [1,2,3]

7

如果你不理解特定答案的工作原理,将你提供的参数与前一步骤中的类型对齐。

Prelude> :t (fmap . fmap) sum
(fmap . fmap) sum
  :: (Functor f, Functor f1, Num b) => f (f1 [b]) -> f (f1 b)

为了使这个工作正常运行,Just 必须有类型 f (f1 [b]),然后 (fmap . fmap) sum Just 必须具有类型 f (f1 b)

Just :: (Functor f, Functor f1, Num b) => f (f1 [b])

这里不太清楚ff1应该是什么,所以我们尝试使用右侧的值。我们可以通过 GHCi 来检查 (fmap . fmap) sum Just 的实际值:

Prelude> :t (fmap . fmap) sum Just
(fmap . fmap) sum Just :: Num b => [b] -> Maybe b

但是这个 应该 匹配:

(Functor f, Functor f1, Num b) => f (f1 b)

我们正在尝试弄清楚这里的ff1是什么。因此,我们必须稍微重写一下它,使其具有相同的结构(请记住,->是语法糖,有时会妨碍我们):

(fmap . fmap) sum Just :: Num b => [b] -> Maybe b
-- Same as...
(fmap . fmap) sum Just :: Num b => (->) [b] (Maybe b)
-- Or...
(fmap . fmap) sum Just :: Num b => ((->) [b]) (Maybe b)
--                     Functor f = ((->) [b])
--                                Functor f1 = Maybe

因此,我们可以得出结论,为了使类型匹配,Functor f必须是(->) [b]…请记住,函数也是函子!而Functor f1Maybe,这更加明显。我们可以进行测试:
Prelude> :t (fmap . fmap) sum :: Num b => ([b] -> Maybe [b]) -> ([b] -> Maybe b)
(fmap . fmap) sum :: Num b => ([b] -> Maybe [b]) -> ([b] -> Maybe b)
  :: Num b => ([b] -> Maybe [b]) -> [b] -> Maybe b

而 GHCi 认为它的类型检查非常好。

这里唯一容易忘记的部分是 (->) [b] 是一个有效的函子!


由于sum现在具有更通用的类型,您可能希望为其提供一个类型签名。 - dfeuer
在这种情况下,它只是 Num a => [a] -> a,更一般的类型签名并不改变事情。 - Dietrich Epp

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