Haskell的类型系统如何产生此错误?

时间:2019-06-07 01:16:21

标签: haskell

在通过Haskell进行的尝试中,我发现当我在代码中的类型上犯了一个错误时,我很难尝试解析我做错了什么,而编译器正在抱怨。我认为这是由于在编译器发现错误之前的部分类型推断。

当然,我习惯于类型不匹配非常明显的语言。例如与function foo expects an argument of type int, but received string相似。很明显,这是什么意思,我传入了一个字符串,但签名需要一个int。

所以这是一些相对简单的代码,它是一个在给出系数和幂的列表的情况下对多项式求值的函数:

poly :: [Int] -> [Int] -> Double -> Double
poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b

这将产生以下编译器错误输出:

[1 of 1] Compiling Main             ( solution.hs, solution.o )

solution.hs:11:56: error:
    • Couldn't match type ‘Int’ with ‘Double’
      Expected type: [Double] -> [(Double, Double)]
        Actual type: [Double] -> [(Int, Double)]
    • In the second argument of ‘(.)’, namely ‘zip a’
      In the second argument of ‘(.)’, namely
        ‘map (\ (ai, bi) -> ai * (x ** bi)) . zip a’
      In the expression: sum . map (\ (ai, bi) -> ai * (x ** bi)) . zip a
   |
11 | poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b
   |                                                        ^^^^^

solution.hs:11:64: error:
    • Couldn't match type ‘Int’ with ‘Double’
      Expected type: [Double]
        Actual type: [Int]
    • In the second argument of ‘($)’, namely ‘b’
      In the expression:
        sum . map (\ (ai, bi) -> ai * (x ** bi)) . zip a $ b
      In an equation for ‘poly’:
          poly a b x = sum . map (\ (ai, bi) -> ai * (x ** bi)) . zip a $ b
   |
11 | poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b
   |                                                                ^

所以我已经知道这里出了什么问题。 ab的类型为[Int],但我需要它们的类型为[Double]。但是,我不明白的是为什么编译器会说它是什么:

  1. 为什么b在表达式中位于a前面并且同样是错误的呢?
  2. 在最上面的错误中,预期的类型为[(Double, Double)]。很酷,很有意义。但是 actual 类型是[(Int, Double)]呢?如果ab均为[Int]时,双重出现了吗?

无论哪种情况,我认为我真正需要的是引导我逐步了解类型系统最终如何生成这些错误的方法,这样我就可以更好地理解为什么错误消息就是它们。

3 个答案:

答案 0 :(得分:5)

  

为什么b在表达式中位于a前面并且同样是错误的呢?

...为什么不呢?您已经说过自己,他们俩都错了。 GHC还发现它们都错了。告诉你他们都是错的。另外,a并不是真正的“之前”,因为在Haskell中没有“之前”和“之后”的实际概念。如果有的话,它是“从内到外”,而不是“左右”,然后b(仅在($)下)在a之前({{1}下) },zip (.))。没关系,反正。

  

在最上面的错误中,预期的类型为($)。很酷,很有意义。但是实际的类型是[(Double, Double)]呢?如果[(Int, Double)]a均为b时,双重出现了吗?

[Int]的类型应为sum . map _etc . zip a,这是因为类型签名以及它位于[Int] -> Double的左侧。更深入地研究,($)应该是zip a。实际上是[Int] -> [(Double, Double)]。使用参数类型,我们可以选择设置forall b. [b] -> [(Int, b)],从而推论b ~ Int实际上是期望zip a的{​​{1}}(这是正确的),或者我们可以选择设置[Int] -> [(Int, Int)](根据返回类型)并确定实际上为[Double] -> [(Double, Double)]也是为true)。两种方式都会出错。实际上,我认为GHC正在采用第三种方式,类似于第一种,但是我将为您省去细节。

问题的核心是这样的:在Haskell程序中,如果您知道表达式周围或其中的内容类型,则有多种方法可以找出表达式的类型。在类型良好的程序中,所有这些派生彼此一致,而在类型错误的程序中,它们通常以多种方式不同意。 GHC只是选择了其中两个,以希望的方式将它们称为“期望”和“实际”,并抱怨他们不同意。在这里,您找到了第三个派生类,它也与“预期”派生类冲突,但是无论出于何种原因,GHC都选择不对“实际类型”使用派生类。选择要显示的派生并不容易,特别是在Haskell中,尽管一切可能会更好,但在其中允许所有事物影响其他事物的类型。几年前,GHC的一位负责人在更好的错误消息上做了some work,但似乎链接旋转有些微-Haskell-analyzer桥似乎已经从Internet旋转了。

如果您遇到这样的错误,我建议首先不要使用b ~ Double样式。如果您将其写为zip a :: [Double] -> [(Int, Double)],那么遵循我的主要建议会更容易。我不会在这里更改它,但是您应该记住这一点。

_ . _ . ... $ _

您看到一个错误,并且更改内容并不立即显而易见。很好,只需放弃尝试解密象形文字并用_ $ _ $ ... $ _代替RHS的一部分。删除的RHS越多,捕获错误的机会就越大,如果擦除所有错误,错误率将达到95%:

poly :: [Int] -> [Int] -> Double -> Double
poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b

GHC会告诉您,左边的_poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . _ $ _ -- personally, I'd nuke the scary-looking map _lambda, too, -- but I'm also trying to keep this short ,右边的_。添加_a -> [(Double, Double)]

_a

,系统会提示您需要两个zip,并且您会发现使用poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip _ $ _ [Double]无效,因为a(和GHC实际上在错误消息中说了b,因为有时不清楚。然后,您找出解决方法:

a, b :: [Int]

一切都很好。

答案 1 :(得分:2)

  
      
  1. 当表达式中的a出现在b上并且同样错误时,为什么会抱怨呢?
  2.   

嗯,有两个错误。它抱怨zip a,也抱怨b。并按照错误在源中出现的顺序发出这些错误。

  
      
  1. 在最上面的错误中,预期的类型为[(Double, Double)]。很酷,很有意义。但是实际的类型是[(Int, Double)]呢?如果ab均为[Int]时,双重出现了吗?
  2.   

不太快。该错误的实际类型不是[(Int, Double)]。它说zip a是错误的,它说期望的类型是[Double] -> [(Double, Double)],而实际的类型是[Double] -> [(Int, Double)]。这与期望表达式为[(Double, Double)]类型并且实际上为[(Int, Double)]类型的表达式不同。

zip a是一个函数。我们知道应该返回[(Double, Double)](因为其余的组成链过程会返回Double的最终poly结果。

函数zip a的其余参数必须为[Double]类型(按zip类型),以使返回类型适合[(Double, Double)],并且没关系; zip a 可以接受[Double]的论证。

问题是zip a不是类型[Double] -> [(Double, Double)]的函数;由于表达式[Double] -> [(Int, Double)]中使用了a,因此它可以管理的最接近zip 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在类型检查期间所做的工作是查看每个表达式(包括子表达式,在每个嵌套级别),并进行比较:

  1. 表达式必须具有的类型,以适合其上下文(将其称为“期望类型”)
  2. 表达式将要基于其组成部分的类型(称其为“实际类型”)

在此示例中,您可以大致想到以下过程:

    由于sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b的类型签名,
  1. Double必须产生poly
  2. 第一个子表达式是$应用于sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a
    1. $的类型为(a -> b) -> a -> b(实际类型) 1
    2. 我们知道上面的b必须是Double的最终预期收益类型,poly的形式应为$(预期的类型)
    3. (a -> Double) -> a > Double的预期类型和实际类型统一起来
  3. 现在我们知道$的预期类型是sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a的形式。让我们检查其实际类型:
    1. 该组合链中的第一个子表达式a -> Double应用于.(运算符部分表示法是sum,前缀表示法是(sum .)):
      1. (.) sum的类型为.;它在此位置的预期类型为(b -> c) -> (a -> b) -> a -> c
      2. (b -> Double) -> (a -> b) -> a -> Double的类型为sum;其在该位置的预期类型为(Foldable t, Num a) => t a -> a形式(需要b -> Double:✔)
      3. 所以Num Double的类型为(sum .)
    2. 合成链中的下一个子表达式是Foldable t => (a -> t Double) -> a -> Double应用于(sum .)
      1. map (\(ai, bi) -> ai * (x ** bi)) . zip a应该适合map (\(ai, bi) -> ai * (x ** bi)) . zip a。让我们检查其实际类型:
        1. 第一个子表达式是Foldable t => a -> t Double应用于.
        2. map (\(ai, bi) -> ai * (x ** bi))的类型为.;它在此位置的预期类型为(b -> c) -> (a -> b) -> a -> c
        3. 因此,Foldable t => (b -> t Double) -> (a -> b) -> a -> t Double应该适合map (\(ai, bi) -> ai * (x ** bi))
        4. 跳过详细信息,b -> t Double实际上具有类型map (\(ai, bi) -> ai * (x ** bi))(因为从[(Double, Double)] -> [Double]的签名中知道x的类型为Double)。< / li>
        5. 这符合预期的类型poly,并告诉我们b -> t Double实际上是b,而[(Double, Double)]实际上是t(需要[] :✔)
      2. 因此,现在我们知道应用于Foldable []的{​​{1}}的实际类型是.,这意味着map (\(ai, bi) -> ai * (x ** bi))的第二个参数是{{1} }的格式应为(a -> [(Double, Double)]) -> [Double]。让我们检查其实际类型:
        1. .的类型为zip a
        2. a -> [(Double, Double)]zip类型签名中的[a] -> [b] -> [(a, b)]类型
        3. 因此,a的类型类似于[Int]。试图统一期望和实际类型会迫使我们实例化polyzip a,而使[b] -> [(Int, b)]的实际类型为b。但是现在没有更多实例化的类型变量。这是我们发现的第一个实际类型与预期类型不符的地方。因此,我们发出类型错误。在报告类型错误时,我们保留Double实例化为zip a的事实,因为这没有什么不对。因此,预期类型显示为[Double] -> [(Int, Double)],而实际类型显示为b
        4. 进一步深入Double并没有多大意义,因为子组件没有有意义的预期类型,因为我们不知道我们刚刚报告的问题是否是预期类型错误或实际类型错误(我们只知道它们不能同时正确)。我们无法确定问题是[Double] -> [(Double, Double)]不适合[Double] -> [(Int, Double)],还是zip a不适合zip,或者[Int] -> [Double] -> [(Double, Double])是否正确问题是上下文期望a
      3. 但是回过头来,假装[Double]实际上符合其预期类型,我们确定zip a具有“实际类型” [Double] -> [(Double, Double)],这与我们的预期类型{ {1}}
    3. 这意味着zip a的实际类型为map (\(ai, bi) -> ai * (x ** bi)) . zip a,与我们的预期类型[Double] -> [Double]一致(现在我们知道Foldable t => a -> t Double是{{1} })。
  4. 最后一个 允许我们进入下一个顶级表达式,即sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a应用于[Double] -> Double
    1. a -> Double的类型应为a
    2. Double实际上具有类型(sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $)(通过b的签名)
    3. 这是实际类型和预期类型不匹配的第二个位置,因此我们报告第二个类型错误。

1 但不是其 actual 实际类型。说b只是类型为[Double]的函数实际上是一种简化,因为b需要支持普通类型变量无法支持的未提升类型。但我不会在这里讨论。简化过程有99%的时间有效。


您可以看到要确定类型系统如何产生这些错误,需要执行大量步骤。因此,在尝试理解类型错误时,我发现这并不是一个有用的过程。

编译器精确指出了它所谈论的确切表达:包括所有“ [Int]”行的语法分析树的下降,以及使用{{1}的图形指示(通常更有用) }字符。

因此,第一步是考虑周围环境,思考“为什么期望该表达式具有这种类型”。在这种情况下,很清楚为什么poly 应该具有类型$;结果被馈送到最终产生(a -> b) -> a -> b的数学运算中,因此列表中元组的两个元素都必须为$,并且in the second argument of (.)的参数也必须为{{1} }。

第2步是考虑“为什么该表达式实际上具有该类型”。同样,这更加明显。如果为^^^^,那么zip a不可能产生适合[Double] -> [(Double, Double)]的内容。

编译器经过验证的细节,以验证导致这些错误的上下文中的其他所有内容基本上无关紧要;它有效地告诉您一切都很好(通过在此不发出任何类型错误)。

为什么它首先发现Double而不是Double还是为什么抱怨zip a是导致[Double]而不是抱怨{{ 1}}的类型为a :: [Int]。从根本上说,这只是要找到一个预期类型和实际类型不一致的地方,而没有任何方法可以判断哪个是正确的(通常都不是)。

这些事情 是可以理解的,并且当您使用编译器的时间更长时,它们会变得相当直观,但是这些事实很少能真正帮助您理解和修复错误。只需选择它报告的第一个错误,将注意力集中在它正在谈论的表达式上,想一想为什么上下文会使它具有报告的“预期类型”,以及为什么表达式的子部分会使其具有报告的“实际类型”。

与GHC在难以理解的错误消息上的声誉相反,我发现它们的质量要比使用其他语言时通常获得的质量高得多。当您不熟悉它们时,它们会以新格式包含很多信息,因此它们会造成混乱。但是一旦您熟悉它们,它们实际上就会非常好。

实际上,此特定错误消息与您所期望的zip a大致相同,完全是 !仅仅是“函数foo”是_ -> [(Double, _)]运算符(您在同一行上使用了两次,因此它可以准确地标识出它在谈论哪个对象),并且期望的参数是另一个类型复杂的函数。它看起来比您要比较的错误消息类型更复杂的唯一原因是,它将预期/实际部分分为两行以提高可读性(并指出这两种类型中的哪一部分不匹配!)并给出您将了解关于哪个子表达式确切包含错误的详细信息,而不仅仅是说zip a

答案 2 :(得分:0)

拥有

poly :: [Int] -> [Int] -> Double -> Double
poly a b x = sum . map (\(ai, bi) -> ai * (x ** bi)) . zip a $ b
一方面,

    a ::        [ Int]
zip a :: [t] -> [(Int,t)]
x :: Double
x ** bi :: Double
     bi :: Double
                      t ::    Double
zip a   :: [Double] -> [(Int, Double)]                     derived

,但另一方面,

poly a b x          ::                      Double
sum                 ::          [Double] -> Double
    ai * (x ** bi)  ::           Double
   (ai ,       bi)  ::  (Double, Double)
zip a   :: [Double] -> [(Double, Double)]                  expected