【问题标题】:How does fmap fmap apply to functions (as arguments)?fmap fmap 如何应用于函数(作为参数)?
【发布时间】:2015-03-03 06:21:41
【问题描述】:

我试图了解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))

为什么会这样?

【问题讨论】:

  • Num a =&gt; a -&gt; af (x -&gt; y) 没有很好地对齐,你最终会得到 f ~ (-&gt;) aa ~ x -&gt; y,因此是 Num (x -&gt; y)。一个更有趣的可能是fmap ($ [1, 2, 3]) $ fmap fmap $ fmap (+) $ Just 10,它返回Just [11,12,13]。一个更有用的Functor 组合子可能是fmap fmap fmap,它允许您通过两个不同的Functors 列出一个函数:(.:) = fmap fmap fmap(10*) .: [Just 1, Nothing, Just 3] == [Just 10, Nothing, Just 30]
  • 请注意fmap fmap fmap 等价于fmap . fmap,因为外部函子被强制为(-&gt;) (a -&gt; b)(这就是为什么要求fmap fmap fmap 的类型只在其约束中指定两个函子) .
  • 但为什么(fmap fmap (*3)) 会进行类型检查?我想我只是在处理具有相同类型参数的两个函数((fmap fmap (*3))test (*3))方面与 ghci 不同
  • fmap fmap fmap 看起来很奇怪
  • test (*3) 产生类型错误,因为类型变量 ab 在输出类型中不存在。由于有Num (a -&gt; b)约束,所以这个约束在应用这个函数的时候必须要解决(因为后面无法解决)。由于不存在这样的实例,你会在那个时候得到一个类型错误。将其更改为test :: (Functor f) =&gt; f (a -&gt; b) -&gt; (a,b),您将获得test (*3) :: Num (a -&gt; b) =&gt; (a, b)

标签: function haskell types functor function-composition


【解决方案1】:

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

What is happening when I compose * with + in 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 -&gt; yb := g x -&gt; 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 专用于(-&gt;) r(即一个函数)。函数是有效的函子。 (-&gt;) 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. 这很愚蠢,因为您正在将一个函数应用到一个只有一个参数 (*3) :: Num a =&gt; a -&gt; a 的函数上,该函数需要一个带有两个参数 (r -&gt; x -&gt; y) 的输入函数。
  2. 这很危险,因为(*3) 的输出是多态的。因此,编译器不会告诉您您正在做一些愚蠢的事情。幸运的是,由于输出是有界的,你会得到一个类型约束 Num (x -&gt; y),这应该表明你在某个地方出错了。

找出类型,r := a := x -&gt; 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(-&gt;) r 上,我们得到:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多