这段 Haskell 代码为什么能够编译通过?

18

给定:

uncurry :: (a-> b -> c) -> (a,b) -> c    
id :: a -> a

调用 uncurry id 的结果是一个类型为:(b -> c, b) -> c 的函数。

我们如何得到这个结果呢?

你如何使用 id (a -> a) 作为 uncurry 的第一个参数,而 uncurry 需要一个 (a -> b -> c) 函数?

2个回答

26

如果我们从“使类型匹配”的角度来看待问题,那么理解起来会更容易:找出我们需要做什么来id的类型以适应uncurry所需的形状。 因为我们有:

id :: a -> a

我们还有:

id :: (b -> c) -> (b -> c)

当我们在原始的id类型中将a替换为b -> c时,就像计算id 42的类型时可以用Int替换一样,我们可以看到这一点。然后,我们可以去掉右边的括号,因为(->)是右结合的:

id :: (b -> c) -> b -> c

展示了id的类型符合形式a -> b -> c,其中ab -> c。换句话说,我们可以通过特化它已经拥有的通用类型来重塑id的类型以适应所需的形式。

另一种理解这个概念的方法是将uncurry ($) 的类型也视为(b -> c,b) -> c。比较id($)的定义:

id :: a -> a
id a = a

($) :: (a -> b) -> a -> b
($) f x = f x

我们可以使后面的定义更加point-free:

($) f = f

此时,($) 只是 id 更特定类型的一种特殊情况,这一点变得清晰明了。


2
谢谢!这开始有点讲得通了!但我还是觉得有点晕! - ssanj
1
@ssanj:如果有什么安慰的话,写成uncurry id而不是uncurry($)是邪恶的 :) - ehird
2
如果你想特别恶劣,你可以写 (\id` 3)代替$ 3或将id定义为infixr 0 `id`并使用它来替代$,例如 (^2) `id` 1 + 2 * 3`。 - Vitus

8
您如何使用id(a -> a)作为uncurry的第一个参数,而uncurry需要一个(a -> b -> c)函数?
实际上,uncurry需要(a ->(b -> c))函数。您能发现区别吗? :)
省略括号是不好的(嗯,有时候)。它使初学Haskell的人无法理解。当然,在掌握了该语言的一些经验后,您会感觉根本不需要它们。
在这里,只要我们将所有省略的括号显式地写出来,一切就变得清晰明了了:
uncurry :: (a -> (b -> c)) -> ((a,b) -> c)
id      ::  a ->    a

现在,编写 uncurry id 调用时需要将类型 a1 -> a1a2 -> (b -> c) 统一。这很简单,a1 ~ a2 并且 a1 ~ (b -> c)。这只是机械操作,不需要创造性思维。因此,所讨论的 id 实际上具有 a -> a where a ~ (b -> c) 类型,因此 uncurry id 具有类型 (b -> c,b) -> c,通过将 a ~ (b -> c) 替换为 (a,b) -> c 得到。也就是说,它期望一个 b -> c 函数和一个 b 值的一对,并且必须产生一个 c 值。
由于这些类型是最通用的(即关于它们并没有任何了解,因此没有特定的函数可以调用以某种特殊的方式处理问题),在这里产生 c 值的 唯一方式 就是使用 b 值作为参数 调用 b -> c 函数。当然,这就是 ($) 所做的事情。因此,uncurry id == uncurry ($),尽管 id 明显不是 ($)

uncurry f (x,y) = f x y ; id z = z ; uncurry id (x,y) = id x y = x y ; uncurry id = (x,y) -> x y ; uncurry id :: (a -> b, a) -> b。uncurry f (x,y) = f x y; id z = z; uncurry id (x,y) = id x y = x y; uncurry id = (x,y) -> x y; uncurry id :: (a -> b, a) -> b。 - Will Ness

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接