为什么两个具有不同类型变量的多态高阶函数的类型相同?

时间:2017-10-27 11:07:39

标签: haskell higher-order-functions parametric-polymorphism

来自Javascript我明白Haskell的列表类型强制实施同类列表。现在让我感到惊讶的是,以下不同的功能类型符合这一要求:

f :: (a -> a) -> a -> a 
f g x = g x

g :: (a -> b) -> a -> b 
g h x = h x

let xs = [f, g] -- type checks

即使g比[{1}}更广泛适用:

f

f(\x -> [x]) "foo" -- type error g(\x -> [x]) "foo" -- type checks 不应该(a -> a)不明确地对待(a -> b)。在我看来,好像后者是前者的子类型。但是Haskell中没有子类型关系,对吧?那为什么这样呢?

2 个答案:

答案 0 :(得分:12)

Haskell是静态类型的,但这并不代表它的Fortran。每种类型必须在编译时修复,但不一定在单个定义中。 fg的类型均为polymorphic。解释这一点的一种方法是f不仅仅是一个函数,而是一整套重载函数。喜欢(在C ++中)

int f (function<int(int)> g, int x) { return g(x); }
char f (function<char(char)> g, char x) { return g(x); }
double f (function<double(double)> g, double x) { return g(x); }
...

当然,实际生成所有这些函数是不切实际的,所以在C ++中你要把它写成模板 < / p>

template <typename T>
T f (function<T(T)> g, T x) { return g(x); }

...意思是,只要编译器找到f项目的代码,它就会找出具体案例中T的内容,然后创建一个具体的< em>模板实例化(一个固定到那个具体类型的单态函数,就像我上面写的那个例子),只在运行时使用那个具体的实例化。

这两个模板函数的具体实例可能具有相同的类型,即使模板看起来有点不同。

现在,Haskell的参数多态性与C ++模板的解析略有不同,但至少在您的示例中它们相同:g是一整套函数,包括实例化{ {1}}(与g :: (Int -> Char) -> Int -> Char的类型不兼容),但f也是g :: (Int -> Int) -> Int -> Int。当您将fg放在一个列表中时,编译器会自动意识到只有类型与g兼容的f子系列才与此相关。

是的,这确实是一种子类型。当我们说“Haskell没有子类型”时,我们的意思是任何具体的(Rank-0)类型与所有其他Rank-0类型不相交,但是多态类型可能重叠。

答案 1 :(得分:1)

@ leftroundabout的答案很扎实;这是一个更技术性的补充答案。

在Haskell中工作的一种子类型关系:系统F“通用实例”关系。这是编译器在根据其签名检查函数的推断类型时使用的内容。基本上,函数的推断类型必须至少与其签名一样多态:

f :: (a -> a) -> a -> a
f g x = g x

此处,f的推断类型为forall a b. (a -> b) -> a -> b,与您提供的g定义相同。但签名更具限制性:添加约束a ~ ba等于b)。

Haskell通过首先用 Skolem类型变量替换签名中的类型变量来检查这一点 - 这些是仅与自身(或类型变量)统一的新的唯一类型常量。我将使用符号$a来表示Skolem常数。

forall a. (a -> a) -> a -> a
($a -> $a) -> $a -> $a

当您意外地有一个“逃避其范围”的类型变量时,您可能会看到对“rigid,Skolem”类型变量的引用:它在引入它的forall量词之外使用。

接下来,编译器执行包含检查。这与类型的正常统一基本相同,a -> b ~ Int -> Char给出a ~ Intb ~ Char;但由于它是一种子类型关系,它也考虑了函数类型的协方差和逆变。如果(a -> b)(c -> d)的子类型,则b必须是d(协变)的子类型,但a必须是超类型< / {> c(逆变)。

{-1-}(a -> b) -> {-2-}(a -> b)  <:  {-3-}($a -> $a) -> {-4-}($a -> $a)

{-3-}($a -> $a) <: {-1-}(a -> b)  -- contravariant (argument)
{-2-}(a -> b) <: {-4-}($a -> $a)  -- covariant (result)

编译器生成以下约束:

$a <: a  -- contravariant
b <: $a  -- covariant
a <: $a  -- contravariant
$a <: b  -- covariant

通过统一来解决它们:

a ~ $a
b ~ $a
a ~ $a
b ~ $a

a ~ b

因此,推断类型(a -> b) -> a -> b至少与签名(a -> a) -> a -> a一样多态。

当你写xs = [f, g]时,正常的统一开始了:你有两个签名:

forall a.   (a -> a) -> a -> a
forall a b. (a -> b) -> a -> b

这些是实例化的,带有新的类型变量:

(a1 -> a1) -> a1 -> a1
(a2 -> b2) -> a2 -> b2

然后统一:

(a1 -> a1) -> a1 -> a1  ~  (a2 -> b2) -> a2 -> b2
a1 -> a1  ~  a2 -> b2
a1 -> a1  ~  a2 -> b2
a1 ~ a2
a1 ~ b2

最后解决了&amp;广义:

forall a1. (a1 -> a1) -> a1 -> a1

因此g的类型不太通用,因为它被限制为与f具有相同的类型。因此,xs的推断类型将为[(a -> a) -> a -> a],因此您在编写[f (\x -> [x]) "foo" | f <- xs]时会遇到与编写f (\x -> [x]) "foo"相同的类型错误;即使g更为一般,你也隐藏了一些普遍性。

现在你可能想知道为什么你会为一个函数提供一个比必要更严格的签名。答案是 - 引导类型推断并产生更好的错误消息。

例如,($)的类型为(a -> b) -> a -> b;但实际上这是id :: c -> c的限制性更强的版本!只需设置c ~ a -> b即可。所以实际上你可以编写foo `id` (bar `id` baz quux)而不是foo $ bar $ baz quux,但是拥有这个专门的身份函数会让编译器明白你期望使用它来将函数应用于参数,所以如果你犯了错误,它可以提前纾困并给你一个更具描述性的错误信息。