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

时间:2014-04-26 05:56:08

标签: haskell clojure polymorphism

我选择使用Haskell的原因是因为rich type system。这在编译时为我提供了有关我的程序的更多信息,帮助我确信它是合理的。

此外,看起来Haskell是一种最佳语言,可以接近expression problemHaskell typeclasses can dispatch on return type。 (与Clojure协议相反 - 它只能在第一个参数上调度)。

当我探索像read这样的Haskell多态返回值函数时:

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

使用以下程序:

addFive :: Int -> Int
addFive x = x + 5

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

我得到the following result

Runtime error
...
prog: Prelude.read: no parse

所以我似乎在使用优秀类型系统的语言中获得运行时错误。

将此与Clojure中的等效代码进行对比:

(defn add-five [x] (+ 5 x))

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

这会给the following result

16
11

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

3 个答案:

答案 0 :(得分:5)

该运行时错误与多态无关,而且与"11"函数无法将字符串read解析为字符列表这一事实有关。

以下是有效的方法。请注意,"11"在运行时可以解析为Int"\"Some More String\""可以在运行时解析为字符串。

print $ 5 + read "11"
print $ "Some string" ++ read "\"Some More String\""

以下是一些不起作用的事情。它们不起作用,因为"Not an integer"无法解析为Int"11"无法解析为字符串。

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

正如answer to your previous question中指出的那样,类型信息已在编译时推断出来。已选择read个功能。想象一下,如果我们分别为readInt :: String -> IntreadString :: String -> String read实例的Read函数提供了两个函数IntString。编译器在编译时已将read的出现替换为原始的各自函数:

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

这一定必须在编译时发生,因为在编译时消除了类型信息,正如您在上一个问题的答案中所解释的那样。

答案 1 :(得分:4)

这里的问题的一部分是在Haskell中可以定义部分函数,​​即可能在某些输入上失败的函数。示例包括readheadtail。非穷举模式匹配是这种偏袒的常见原因,其他包括errorundefined和无限递归(即使在这种情况下,您显然没有得到运行时错误)。

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

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")实际上是伪装的函数,因为它们取决于它们被评估的类型,如上例所示。单态限制试图使它们更像非函数:它强制编译器为多态值的所有使用找到单一类型。如果可以,则仅在该类型下评估多态值,并且可以在所有用途中共享结果。否则,即使代码在没有限制的情况下可以输入,也会出现类型错误。

例如,以下代码

main = do
  let x = readMaybe "11"
  print $ x :: Maybe Int
  print $ x :: Maybe Int

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

main = do
  let x = readMaybe "11"
  print $ x :: Maybe Int
  print $ x :: Maybe String
如果启用了单态限制,

会引发编译时类型错误,如果关闭则编译并运行正常(打印“Just 11”和“Nothing”)。

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

答案 2 :(得分:2)

read的类型是

(Read a) => String -> a

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

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

这个选择发生在编译时,这意味着在编译之后,你的程序等于

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

但是这个readString无法将其参数"11"解析为字符串,因此它在运行时崩溃。

解决这个问题很简单:

main = do
    print (addFive (read "11"))
    putStrLn (read "\"11\"")