我试图了解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 -> y
和b := 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
的上下文中。假设f
是Maybe
.因此,在专业化方面:
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
(即函数(。函数是有效的函子。(->) r
的fmap
函数只是函数组合。因此,专业化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
你明白为什么它是愚蠢和危险的吗?
- 这是愚蠢的,因为您正在将一个期望具有两个参数(
r -> x -> y
(的输入函数应用于只有一个参数的函数,(*3) :: Num a => a -> a
。 - 这很危险,因为
(*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)
。如果输出类型中出现a
或b
,则可以对它们进行专用处理,并且可以选择不同的Num (a -> b)
实例。但是,由于它们不会出现在输出类型中,因此编译器必须决定立即选择哪个Num (a -> b)
实例;由于Num (a -> b)
是一个没有任何实例的愚蠢约束,因此编译器会抛出错误。
如果您尝试test (*)
那么出于与我上面提到的相同原因,您不会收到任何错误。