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

egd*_*try 4 haskell types function functor function-composition

我试图了解如何fmap fmap适用于 say 之类的函数(*3)

的类型fmap fmap

(fmap fmap):: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b)
Run Code Online (Sandbox Code Playgroud)

类型(*3)

(*3) :: Num a => a -> a
Run Code Online (Sandbox Code Playgroud)

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

Prelude> :t (fmap fmap (*3))
(fmap fmap (*3)):: (Num (a -> b), Functor f) => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

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

test :: (Functor f) => f (a -> b) -> Bool 
test f = True
Run Code Online (Sandbox Code Playgroud)

(*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))
Run Code Online (Sandbox Code Playgroud)

为什么会这样?

Aad*_*hah 5

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

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

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

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|    |________|
                         |            |
                      domain      codomain
Run Code Online (Sandbox Code Playgroud)

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

“域”和“共域”这两个词分别表示“输入”和“输出”。无论如何,让我们看到,当我们申请会发生什么fmapfmap

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|
                         |
                       fmap :: Functor g => (x -> y) -> g x -> g y
                                            |______|    |________|
                                                |            |
                                                a    ->      b
Run Code Online (Sandbox Code Playgroud)

如您所见,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)
Run Code Online (Sandbox Code Playgroud)

那么,有什么作用fmap fmap呢?第一个fmap将第二个提升fmap到函子的上下文中f。让我们说fMaybe。因此,在专业方面:

fmap fmap :: Functor g => Maybe (x -> y) -> Maybe (g x -> g y)
Run Code Online (Sandbox Code Playgroud)

因此fmap fmap必须应用于Maybe其中包含函数的值。是什么fmap fmap做的是,它提升内部的函数Maybe值到另一个函子的情况下g。让我们说g[]。因此,在专业方面:

fmap fmap :: Maybe (x -> y) -> Maybe ([x] -> [y])
Run Code Online (Sandbox Code Playgroud)

如果我们申请fmap fmapNothing那么我们得到Nothing。然而,如果我们将它应用到Just (+1)那么我们会得到一个函数,它增加列表的每个数字,包装在Just构造函数中(即我们得到Just (fmap (+1)))。

不过fmap fmap比较一般。它实际上做了什么,它查看了一个函子f(无论f是什么)并将里面的函数提升f到另一个函子的上下文中g

到现在为止还挺好。所以有什么问题?问题是,当你申请fmap fmap(*3)。这是愚蠢和危险的,就像酒后驾车一样。让我告诉你为什么这是愚蠢和危险的。看一下 的类型签名(*3)

(*3) :: Num a => a -> a
Run Code Online (Sandbox Code Playgroud)

当您申请fmap fmap(*3),函子f专用于(->) r(即一个函数)。一个函数是一个有效的函子。fmapfor的函数(->) r只是函数组合。因此,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
Run Code Online (Sandbox Code Playgroud)

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

  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
Run Code Online (Sandbox Code Playgroud)

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

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

你真正想做的是 apply fmap fmapto (*),它是一个二元函数:

(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                         |___________|
                               |
                              (*) :: Num a => a -> a -> a
                                              |    |    |
                                              r -> x -> y
Run Code Online (Sandbox Code Playgroud)

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

fmap . (*) :: (Num a, Functor g) => a -> g a -> g a
Run Code Online (Sandbox Code Playgroud)

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

  fmap . (*)
= \x -> fmap (x *)
Run Code Online (Sandbox Code Playgroud)

因此,fmap fmap (*) 3很简单fmap (3*)

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

test :: Functor f => f (a -> b) -> Bool
Run Code Online (Sandbox Code Playgroud)

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

test :: (r -> a -> b) -> Bool
        |___________|
              |
            (*3) :: Num x => x ->    x
                             |       |
                             |    ------
                             |    |    |
                             r -> a -> b
Run Code Online (Sandbox Code Playgroud)

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

test (*3) :: Num (a -> b) => Bool
Run Code Online (Sandbox Code Playgroud)

由于既没有a也没有b出现在输出类型中,Num (a -> b)必须立即解决约束。如果ab出现在输出类型中,那么它们可以被专门化并且Num (a -> b)可以选择不同的实例。但是,因为它们没有出现在输出类型中,所以编译器必须立即决定选择哪个实例Num (a -> b);因为Num (a -> b)是一个没有任何实例的愚蠢约束,编译器会抛出一个错误。

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