关于clojure:为什么Haskell推断类型的返回类型多态会导致运行时错误?

Why do Haskell inferred types in return type polymorphism lead to runtime errors?

我之所以选择使用haskell,是因为它有丰富的类型系统。这在编译时为我提供了关于程序的更多信息,帮助我相信它是合理的。

此外,似乎Haskell是处理表达式问题的最佳语言,因为Haskell类型类可以在返回类型上进行分派。(与clojure协议相反-只能在第一个参数上调度)。

当我探索haskell多态性返回值函数(如read时:

1
read :: (Read a) => String -> a

使用以下程序:

1
2
3
4
5
6
7
addFive :: Int -> Int
addFive x = x + 5

main :: IO ()
main = do
    print (addFive (read"11"))
    putStrLn (read"11")

我得到以下结果:

1
2
3
Runtime error
...
prog: Prelude.read: no parse

所以我似乎在使用高级类型系统的语言中得到了一个运行时错误。

对比一下Clojure中的等效代码:

1
2
3
4
(defn add-five [x] (+ 5 x))

(println (add-five (read-string"11")))
(println (read-string"11"))

结果如下:

1
2
16
11

我的问题是,为什么Haskell在返回类型多态性中推断类型会导致运行时错误?它不应该在编译时提取它们吗?


运行时错误与多态性无关,所有的事情都与字符串"11"不能被read函数解析为字符列表这一事实有关。

这里有一些有用的东西。注意,"11"可以在运行时被解析为Int"\"Some More String\""可以在运行时被解析为字符串。

1
2
print $ 5 + read"11"
print $"Some string" ++ read""Some More String""

以下是一些不起作用的东西。它们不起作用,因为"Not an integer"不能被解析为Int"11"不能被解析为字符串。

1
2
print $ 5 + read"Not an integer"
print $"Some string" ++ read"11"

正如前面问题的答案中指出的,类型信息已经在编译时被推断出来了。已经选择了read功能。假设我们有两个函数readInt :: String -> IntreadString :: String -> String,分别为read函数和String实例提供read函数。在编译时,编译器已将出现的read替换为原始的各自函数:

1
2
print $ 5 + readInt"Not an integer"
print $"Some string" ++ readString"11"

这一定是在编译时发生的,因为类型信息在编译时被消除,正如前面问题的答案中所解释的那样。


这里的一个问题是,在haskell中,可以定义部分函数,即在某些输入上可能失败的函数。例如readheadtail。非穷尽模式匹配是造成这种偏袒的常见原因,其他原因包括errorundefined和无限递归(即使在这种情况下,显然不会出现运行时错误)。

特别是,read有点讨厌,因为它要求您确保可以解析字符串。例如,这通常比确保列表不为空要困难。应该使用更安全的变体,如

1
2
3
4
5
readMaybe :: Read a => String -> Maybe a

main = do
  print $ readMaybe"11" :: Maybe Int     -- prints Just 11
  print $ readMaybe"11" :: Maybe String  -- prints Nothing

问题的另一部分是,多态值(如read"11")实际上是伪装的函数,因为它们取决于它们被评估的类型,如上例所示。单态限制试图使它们更像非函数:它强制编译器为多态值的所有使用找到一个单一的类型。如果可能的话,多态性值只在该类型上进行评估,并且结果可以在所有用途中共享。否则,您会得到一个类型错误,即使代码可以在没有限制的情况下被类型化。

例如,以下代码

1
2
3
4
main = do
  let x = readMaybe"11"
  print $ x :: Maybe Int
  print $ x :: Maybe Int

如果打开了单态限制,则解析一次11,如果关闭了,则解析两次(除非编译器足够聪明,可以进行一些优化)。相比之下,

1
2
3
4
main = do
  let x = readMaybe"11"
  print $ x :: Maybe Int
  print $ x :: Maybe String

如果启用了单态限制,则引发编译时类型错误;如果禁用了限制,则编译并运行正常(打印"just 11"和"nothing")。

因此,在启用和禁用限制之间没有明确的赢家。


read的类型是

1
(Read a) => String -> a

这意味着它(实际上是编译器或解释器)将根据上下文的要求选择其返回类型。

因此,在addFive (read"11")中,由于addFive需要Int,编译器选择的read类型为String -> Int;在putStrLn (read"11")中,由于putStrLn需要String,所以String->String

这个选择发生在编译时,也就是说在编译之后,你的程序

1
2
3
main = do
    print (addFive (readInt"11"))
    putStrLn (readString"11")

但是这个readString不能把它的参数"11"解析为字符串,所以它在运行时崩溃。

解决这个问题很简单:

1
2
3
main = do
    print (addFive (read"11"))
    putStrLn (read""11"")