fmap fmap 如何应用于函数(作为参数)



我试图了解fmap fmap如何应用于像 (*3) 这样的函数。

fmap fmap类型:

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

(*3)类型 :

(*3) :: Num a => a -> a

这意味着签名a -> a对应于f (a -> b),对吧?

Prelude> :t (fmap fmap (*3))
(fmap fmap (*3)):: (Num (a -> b), Functor f) => (a -> b) -> f a -> f b

我尝试创建一个简单的测试:

test :: (Functor f) => f (a -> b) -> Bool 
test f = True

并喂(*3)进去,但我得到:

*Main> :t (test (*3))
<interactive>:1:8:
    No instance for (Num (a0 -> b0)) arising from a use of ‘*’
    In the first argument of ‘test’, namely ‘(* 3)’
    In the expression: (test (* 3))

为什么会这样?

当你不知道自己在做什么时,多态性是危险的。fmap(*)都是多态函数,盲目使用它们会导致非常混乱(可能不正确(的代码。我之前回答过类似的问题:

当我在 Haskell 中用 + 组合 * 时会发生什么?

在这种情况下,我相信查看值的类型可以帮助您找出出错的地方以及如何纠正问题。让我们从fmap的类型签名开始:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|    |________|
                         |            |
                      domain      codomain

fmap的类型签名很容易理解。它将函数从a提升到b到函子的上下文中,无论该函子是什么(例如列表,也许,两者都等(。

"域"和">

共域"这两个词分别表示"输入"和"输出"。无论如何,让我们看看当我们fmap应用于fmap时会发生什么:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|
                         |
                       fmap :: Functor g => (x -> y) -> g x -> g y
                                            |______|    |________|
                                                |            |
                                                a    ->      b

如您所见,a := x -> yb := g x -> g y.此外,还添加了Functor g约束。这给了我们fmap fmap的类型签名:

fmap fmap :: (Functor f, Functor g) => f (x -> y) -> f (g x -> g y)

那么,fmap fmap是做什么的呢?第一个fmap将第二个fmap提升到函子f的上下文中。假设fMaybe.因此,在专业化方面:

fmap fmap :: Functor g => Maybe (x -> y) -> Maybe (g x -> g y)

因此,fmap fmap必须应用于包含函数的Maybe值。fmap fmap的作用是它将Maybe值内的函数提升到另一个函子g的上下文中。假设g[].因此,在专业化方面:

fmap fmap :: Maybe (x -> y) -> Maybe ([x] -> [y])

如果我们将fmap fmap应用于Nothing,那么我们会得到Nothing。但是,如果我们将其应用于Just (+1)那么我们会得到一个函数,该函数将列表的每个数字都包含在Just构造函数中(即我们得到Just (fmap (+1))(。

但是,fmap fmap更笼统。它实际上做了什么,它查看函子内部f(无论f是什么(,并将f内部的函数提升到另一个函子g的上下文中。

目前为止,一切都好。那么问题出在哪里呢?问题是当您将fmap fmap应用于(*3)时。这是愚蠢和危险的,就像酒后驾车一样。让我告诉你为什么它是愚蠢和危险的。看看(*3)的类型签名:

(*3) :: Num a => a -> a

当您将fmap fmap应用于(*3)时,函子f专门用于(->) r(即函数(。函数是有效的函子。(->) rfmap函数只是函数组合。因此,专业化fmap fmap的类型是:

fmap fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
-- or
(.)  fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                          |___________|
                                |
                              (*3) :: Num a => a ->    a
                                               |       |
                                               |    ------
                                               |    |    |
                                               r -> x -> y

你明白为什么它是愚蠢和危险的吗?

  1. 这是愚蠢的,因为您正在将一个期望具有两个参数(r -> x -> y(的输入函数应用于只有一个参数的函数,(*3) :: Num a => a -> a
  2. 这很危险,因为(*3)的输出是多态的。因此,编译器不会告诉您您正在做一些愚蠢的事情。幸运的是,由于输出是有界的,因此您会得到一个类型约束Num (x -> y)该约束应指示您在某处出错。

计算类型,r := a := x -> y.因此,我们得到以下类型签名:

fmap . (*3) :: (Num (x -> y), Functor g) => (x -> y) -> g x -> g y

让我告诉你为什么使用值是错误的:

  fmap . (*3)
= x -> fmap (x * 3)
             |_____|
                |
                +--> You are trying to lift a number into the context of a functor!

你真正想做的是将fmap fmap应用于(*),这是一个二进制函数:

(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                         |___________|
                               |
                              (*) :: Num a => a -> a -> a
                                              |    |    |
                                              r -> x -> y

因此,r := x := y := a.这为您提供了类型签名:

fmap . (*) :: (Num a, Functor g) => a -> g a -> g a

当您看到这些值时,这更有意义:

  fmap . (*)
= x -> fmap (x *)

因此,fmap fmap (*) 3简直就是fmap (3*).

最后,您的test函数也有同样的问题:

test :: Functor f => f (a -> b) -> Bool

在专门的函子f (->) r我们得到:

test :: (r -> a -> b) -> Bool
        |___________|
              |
            (*3) :: Num x => x ->    x
                             |       |
                             |    ------
                             |    |    |
                             r -> a -> b

因此,r := x := a -> b.因此,我们得到类型签名:

test (*3) :: Num (a -> b) => Bool

由于输出类型中既不显示a也不b,因此必须立即解决约束Num (a -> b)。如果输出类型中出现ab,则可以对它们进行专用处理,并且可以选择不同的Num (a -> b)实例。但是,由于它们不会出现在输出类型中,因此编译器必须决定立即选择哪个Num (a -> b)实例;由于Num (a -> b)是一个没有任何实例的愚蠢约束,因此编译器会抛出错误。

如果您尝试test (*)那么出于与我上面提到的相同原因,您不会收到任何错误。

最新更新