【问题标题】:Understanding Haskell Type Signatures了解 Haskell 类型签名
【发布时间】:2014-04-15 17:49:59
【问题描述】:

我正在自学 Haskell,我想知道以下类型签名:

Prelude> :t ($)
($) :: (a -> b) -> a -> b
Prelude>

我应该如何解释(不是双关语)?

一个半相似的结果也被证明是令人费解的:

Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude>

【问题讨论】:

    标签: haskell types type-signature


    【解决方案1】:

    ($) 只是函数应用程序。它获取一个a->b 类型的函数,一个a 类型的参数,应用该函数并返回一个b 类型的值。

    map 是一个很好的例子,说明阅读函数类型签名如何帮助理解它。 map 的第一个参数是一个接受a 并返回b 的函数,它的第二个参数是一个[a] 类型的列表。 所以mapa->b 类型的函数应用于a 值列表。结果类型确实是[b] 类型——b 值的列表!

    (a->b)->[a]->[b] 可以解释为“接受一个函数和一个列表并返回另一个列表”,也可以解释为“接受一个a->b 类型的函数并返回另一个[a]->[b] 类型的函数”。 当你这样看时,map "upgrade" f (在这种情况下经常使用术语 "lift")来处理列表:如果 double 是一个将整数加倍的函数,那么 map double 是将列表中的每个整数加倍的函数。

    【讨论】:

    • a -> b 括在括号中是否重要?它似乎表示优先级。
    • 你是对的!箭头是右结合的,所以 a->b->c 等于 a->(b->c) - 这是一个接受 a 类型参数并返回另一个 b->c 类型函数的函数。如果你想要一个函数作为参数,你需要括号。
    • 是的,这很重要。优先级通常是从右到左,这样f :: a -> b -> cf :: a -> (b -> c) 是等价的。这实际上是因为 Haskell 函数可以部分应用(将 f 应用于某些 a 会返回从 b 到 c 的函数)。当使用g :: (a -> b) -> c 代替时,它特别表示您必须将函数应用于g,并且您不能部分应用g。
    • 感谢大家的帮助。为了巩固我的理解,如果有人写: map (*2) [1..50] 并检查了 map 的类型签名, (a -> b) 分别对应于 map 函数和 (*2)? [a] 是从 1 到 50 的列表,而 [b] 是结果?看起来好像我们在用括号强制左关联。 (a -> b) 返回一个接受“a”类型列表并返回“b”类型列表的函数?这会是一个准确的解释吗?
    • (a -> b) 对应于(*2),因为在我们的示例中abInt,而(*2)::Int -> Int(a -> b) 是一个接受a 类型的单个值并返回b 类型的单个值的函数。但是map (*2),由于部分应用,属于[a]->[b] 类型——接受a 列表并返回b 列表。
    【解决方案2】:

    我将从map 开始。 map 函数将操作应用于列表中的每个元素。如果我有

    add3 :: Int -> Int
    add3 x = x + 3
    

    然后我可以使用map 将其应用于Ints 的整个列表:

    > map add3 [1, 2, 3, 4]
    [4, 5, 6, 7]
    

    所以如果你看一下类型签名

    map :: (a -> b) -> [a] -> [b]
    

    您会看到第一个参数是(a -> b),它只是一个接受a 并返回b 的函数。第二个参数是[a],它是a 类型值的列表,返回类型[b],是b 类型值的列表。所以用简单的英语来说,map 函数将一个函数应用于值列表中的每个元素,然后将这些值作为列表返回。

    这就是使map 成为高阶函数的原因,它将函数作为参数并用它来处理。另一种查看map 的方法是在类型签名中添加一些括号来实现

    map :: (a -> b) -> ([a] -> [b])
    

    所以你也可以把它想象成一个函数,将一个函数从a 转换为b 到一个从[a][b] 的函数。


    函数($)具有类型

    ($) :: (a -> b) -> a -> b
    

    和使用一样

    > add3 $ 1 + 1
    5
    

    它所做的只是将 right 的内容(在本例中为 1 + 1)传递给 left 上的函数,此处为 add3。为什么这很重要?它有一个方便的 fixity 或运算符优先级,这使得它等同于

    > add3 (1 + 1)
    

    所以右边的任何东西在被传递到左边之前基本上都被包裹在括号中。这只是使它对于将多个函数链接在一起很有用:

    > add3 $ add3 $ add3 $ add3 $ 1 + 1
    

    > add3 (add3 (add3 (add3 (1 + 1))))
    

    因为您不必关闭括号。

    【讨论】:

    • add3 $ add3 $ add3 $ add3 $ 1 + 1add3 . add3 . add3 . add3 $ 1 + 1 之间有什么实际区别吗?
    • 如果签名(a -> b) -> [a] -> [b](a -> b) -> ([a] -> [b]) 是等价的,这是否意味着-> 是右关联的?
    • @Ryan 没错,函数箭头是右关联的。 a -> b -> c -> d ~= a -> (b -> (c -> d)),但 ((a -> b) -> c) -> d 不能再减少了。
    【解决方案3】:

    好吧,如前所述,$ 可以很容易理解,如果您忘记了柯里化并看到它就像在 C++ 中一样

    template<typename A, typename B>
    B dollar(std::function<B(A)> f, A x) {
      return f(x);
    }
    

    但实际上,这不仅仅是将函数应用于值! $map 的签名之间的明显相似性实际上具有非常深刻的范畴论意义:两者都是函子的态射作用的例子!

    在我们一直使用的 Hask 类别中,对象是类型。 (That is a bit confusionsome,但别担心)。态射是函数。

    最著名的 (endo-)函子 是那些具有 the eponymous type class 实例的函子。但实际上,在数学上,函子只是将对象映射到对象和态射到态射的东西1map(我想是双关语!)就是一个例子:它接受一个对象(即类型)A 并将其映射到类型[A]。而且,对于AB 的任意两种类型,它需要一个态射(即函数)A -&gt; B,并将其映射到相应的[A] -&gt; [B] 类型的列表函数。

    这只是仿函数类签名操作的一个特例:

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

    数学并不要求这个fmap 有一个名字。因此也可以有 identity functor,它简单地将任何类型分配给自身。而且,每个态射对自身:

    ($) :: (a->b) -> (a->b)
    

    “身份”显然存在更普遍,你也可以将任何类型的值映射到自己。

    id :: a -> a
    id x = x
    

    果然,一个可能的实现是那么

    ($) = id
    

    1头脑,而不是任何东西,映射对象和态射是函子......它确实需要满足functor laws

    【讨论】:

    • 非常有趣。谢谢!
    猜你喜欢
    • 2016-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-22
    • 2016-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多