- 当表达式中的 a 出现在
b 之前并且同样错误时,为什么它会抱怨 b?
嗯,有两个错误。它抱怨zip a,它抱怨b。它会按照它们在源代码中出现的顺序发出这些错误。
- 在最上面的错误中,预期的类型是
[(Double, Double)]。酷,有道理。但是实际类型怎么会是[(Int, Double)]?如果a 和b 都是[Int],那么双重是如何出现的?
没那么快。该错误中的实际类型不是[(Int, Double)]。它说zip a 是错误的,它说预期类型是[Double] -> [(Double, Double)],而实际类型是[Double] -> [(Int, Double)]。这与预期类型为 [(Double, Double)] 而实际上类型为 [(Int, Double)] 的表达式不同。
zip a 是一个函数。我们知道它应该返回[(Double, Double)](因为组合链的其余部分会处理从poly 返回最终的Double 结果。
函数zip a 的剩余参数必须是[Double] 类型(zip 类型)才能使返回类型适合[(Double, Double)],这很好; zip a 可以接受[Double] 参数。
问题是zip a不是[Double] -> [(Double, Double)]类型的函数;它可以管理的最接近的是[Double] -> [(Int, Double)],因为在表达式zip a 中使用了a。所以这就是错误所抱怨的。
你在问为什么它不抱怨[(Int, Int)] 而不是[(Int, Double)],因为如果你将zip a 应用到b 就会得到这样的结果。但是您的代码中没有任何地方这样做!您将整个函数sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a 应用于b(通过$ 运算符)。类型错误不是抱怨zip a b 是错误的列表类型,而是抱怨zip a 是错误的函数类型。
然后它分别抱怨$ 运算符的第二个参数是也是错误类型作为参数传递给整个函数(其中zip a 是一小部分) ,但这是与其他错误完全不同的问题。
Haskell 在类型检查期间所做的是查看每个表达式(包括子表达式,在每个嵌套级别),并进行比较:
- 表达式必须具有的类型,以适应其上下文(它称之为“预期类型”)
- 表达式将基于其组件的类型(它称之为“实际类型”)
你可以大致认为这个例子中的过程如下:
-
sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b 必须导致 Double,因为 poly 的类型签名
- 第一个子表达式是
$ 应用于sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a
-
$ 具有类型 (a -> b) -> a -> b(实际类型)1
- 我们知道上面的
b必须是Double,最终的预期返回类型为poly,所以$应该是(a -> Double) -> a > Double(预期类型)的形式
-
$ 的预期和实际类型统一没有问题
- 现在我们知道
sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a 的预期类型是a -> Double 的形式。让我们检查它的实际类型:
- 该组合链中的第一个子表达式是
.,应用于sum((sum .) 在运算符部分表示法中,或(.) sum 在前缀表示法中):
-
. 具有 (b -> c) -> (a -> b) -> a -> c 类型;它在这个位置的预期类型是(b -> Double) -> (a -> b) -> a -> Double
-
sum 的类型为 (Foldable t, Num a) => t a -> a;它在这个位置的预期类型是b -> Double(需要Num Double:✔)
- 所以
(sum .) 的类型为Foldable t => (a -> t Double) -> a -> Double
- 组合链中的下一个子表达式是
(sum .),应用于map (\(ai, bi) -> ai * (x ** bi)) . zip a
-
map (\(ai, bi) -> ai * (x ** bi)) . zip a 预计适合 Foldable t => a -> t Double。让我们检查它的实际类型:
- 它的第一个子表达式是
. 应用于map (\(ai, bi) -> ai * (x ** bi))
-
. 的类型为 (b -> c) -> (a -> b) -> a -> c;它在这个位置的预期类型是Foldable t => (b -> t Double) -> (a -> b) -> a -> t Double
- 所以
map (\(ai, bi) -> ai * (x ** bi)) 应该适合b -> t Double
- 略过细节,
map (\(ai, bi) -> ai * (x ** bi)) 实际上具有 [(Double, Double)] -> [Double] 类型(因为从 poly 的签名中已知 x 具有 Double 类型)。
- 这符合预期的类型
b -> t Double,并告诉我们b 实际上是[(Double, Double)] 而t 实际上是[](需要Foldable []:✔)
- 所以现在我们知道
.的实际类型应用于map (\(ai, bi) -> ai * (x ** bi))是(a -> [(Double, Double)]) -> [Double],这意味着.的第二个参数,即zip a,应该是@的形式987654416@。让我们检查它的实际类型:
-
zip 是 [a] -> [b] -> [(a, b)] 类型
-
a 的类型为 [Int],来自 poly 的类型签名
- 所以
zip a 的类型类似于[b] -> [(Int, b)]。试图统一预期类型和实际类型迫使我们将b 实例化为Double,而将zip a 的实际类型留给[Double] -> [(Int, Double)]。但是现在没有更多类型变量可以实例化。这是我们发现的第一个实际类型与预期类型不匹配的地方。所以我们发出一个类型错误。在报告类型错误时,我们保留b 被实例化为Double 的事实,因为这并没有错。所以预期类型显示为[Double] -> [(Double, Double)],实际类型显示为[Double] -> [(Int, Double)]
- 进一步深入
zip a 没有什么意义,因为子组件没有有意义的预期类型,因为我们不知道我们刚刚报告的问题是否是预期类型错误或者实际类型是错误的(我们只知道它们不可能都是正确的)。我们不能说问题是zip 不适合[Int] -> [Double] -> [(Double, Double]),还是a 不适合[Double],或者zip a 实际上是否正确,问题是上下文期待@ 987654438@.
- 但是回过头来假装
zip a确实符合它的预期类型,我们确定map (\(ai, bi) -> ai * (x ** bi)) . zip a具有“实际类型”[Double] -> [Double],它与我们预期的Foldable t => a -> t Double类型一致
- 这意味着
sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a 具有实际类型[Double] -> Double,它与我们预期的a -> Double 类型一致(我们现在知道a 是Double)。
- 这最终允许我们进入下一个顶级表达式,即
(sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $)应用于b。
-
b 应该有 [Double] 类型
-
b 实际上有 [Int] 类型(由 poly 的签名)
- 这是我们的第二个实际类型和预期类型不匹配的地方,所以我们报告第二个类型错误。
1 但不是它的 actual 实际类型。说$只是一个类型为(a -> b) -> a -> b的函数其实是一种简化,因为$需要支持unlifted类型之类的东西,而普通类型变量是无法支持的。但我不打算在这里讨论。这种简化在 99% 的情况下都有效。
您可以看到,要弄清楚类型系统如何生成这些错误,需要执行大量步骤。所以我真的不认为这是一个有助于理解类型错误的过程。
编译器精确地指出了它所谈论的确切表达式:无论是在解析树的下降方面,所有这些“in the second argument of (.)”行,还是(通常更有帮助)在下面使用^^^^ 字符的图形指示。
所以第一步是考虑“为什么那个表达式应该是那种类型”,看看周围的环境。在这种情况下,很清楚为什么zip a 应该 具有[Double] -> [(Double, Double)] 类型;结果被输入最终产生Double的数学运算,因此列表中元组的两个元素都必须是Double,zip a的参数也必须是[Double]。
第 2 步是思考“为什么那个表达式实际上会有那种类型”。同样,这更加明显。如果a :: [Int],那么zip a 不可能产生适合_ -> [(Double, _)] 的东西。
编译器在验证导致这些错误的上下文中的所有其他内容时所经历的挑剔的细节大多是无关紧要的;它有效地告诉你一切都很好(不会在那里发出任何类型错误)。
为什么它首先发现zip a而不是b,或者为什么它抱怨zip a是一个导致[(Int, Double)]的函数而不是抱怨a是[Int]类型也无关紧要。从根本上说,它只是找到一个预期类型和实际类型不一致的地方,没有任何方法可以判断哪个是正确的(通常两者都不是)。
这些东西是可以理解的,并且随着您使用编译器的时间越长,它们变得相当直观,但是这些事实很少能真正帮助您理解和修复错误。只需选择它报告的第一个错误,专注于它正在谈论的表达式,思考为什么上下文会导致它具有报告的“预期类型”,以及为什么表达式的子部分会导致它具有报告的“实际类型”。
与 GHC 以高深莫测的错误消息而闻名的名声相反,我实际上发现它们的质量比我在使用其他语言时通常得到的要高得多。当您不熟悉它们时,它们以新格式包含大量信息,因此它们会令人困惑。但是一旦你熟悉了它们,它们实际上是非常好的。
事实上,这个特定的错误消息完全符合您所期望的function foo expects an argument of type int, but received string!只是“函数 foo”是 . 运算符(您在同一行使用了两次,因此它可以准确识别它正在谈论的那个),它所期望的参数是另一个具有复杂类型的函数。它看起来比您与之比较的错误消息类型更复杂的唯一原因是它将预期/实际部分分成两行以提高可读性(并准确指出这两种类型中的哪一部分不匹配!)并给出您详细说明了哪个子表达式包含错误,而不仅仅是说line 11: function (.) expects an argument of type [Double] -> [(Double, Double)], but received [Double] -> [(Int, Double)]。