稳健 haskell 没有错误

Robust haskell without errors

我目前正在学习 Haskell。我选择这种语言的动机之一是编写具有非常高健壮性的软件,即完全定义的函数、数学确定性的并且永远不会崩溃或产生错误。我的意思不是由系统谓词("system out of memory"、"computer on fire" 等)引起的失败,这些并不有趣,只会让整个过程崩溃。我也不是指由无效声明 (pi = 4) 引起的错误行为。

相反,我指的是由错误状态引起的错误,我想通过严格的静态类型使这些状态不可表示和不可编译(在某些函数中)来消除这些错误。在我看来,我将这些函数称为 "pure" 并认为强类型系统可以让我完成此任务。然而 Haskell 没有以这种方式定义 "pure" 并允许程序在任何上下文中通过 error 崩溃。

Why is catching an exception non-pure, but throwing an exception is pure?

这完全可以接受,一点也不奇怪。然而令人失望的是 Haskell 似乎没有提供一些功能来禁止可能导致分支使用 error.

的函数定义

这是一个人为的例子,为什么我觉得这令人失望:

module Main where
import Data.Maybe

data Fruit = Apple | Banana | Orange Int | Peach
    deriving(Show)

readFruit :: String -> Maybe Fruit
readFruit x =
    case x of
         "apple" -> Just Apple
         "banana" -> Just Banana
         "orange" -> Just (Orange 4)
         "peach" -> Just Peach
         _ -> Nothing

showFruit :: Fruit -> String
showFruit Apple = "An apple"
showFruit Banana = "A Banana"
showFruit (Orange x) = show x ++ " oranges"

printFruit :: Maybe Fruit -> String
printFruit x = showFruit $ fromJust x

main :: IO ()
main = do
    line <- getLine
    let fruit = readFruit line
    putStrLn $ printFruit fruit
    main

假设我偏执地认为纯函数 readFruitprintFruit 确实不会因未处理状态而失败。您可以想象该代码用于发射载满宇航员的火箭,在绝对关键的例程中需要序列化和反序列化水果值。

第一个危险自然是我们在模式匹配中犯了错误,因为这给了我们可怕的无法处理的错误状态。值得庆幸的是 Haskell 提供了内置的方法来防止这些,我们只需使用 -Wall 编译我们的程序,其中包括 -fwarn-incomplete-patterns 和 AHA:

src/Main.hs:17:1: Warning:
    Pattern match(es) are non-exhaustive
    In an equation for ‘showFruit’: Patterns not matched: Peach

我们忘记序列化 Peach fruits 并且 showFruit 会抛出错误。这是一个简单的修复,我们只需添加:

showFruit Peach = "A peach"

程序现在编译时没有警告,避免了危险!我们发射了火箭,但突然程序崩溃了:

Maybe.fromJust: Nothing

火箭注定要坠入大海,原因如下:

printFruit x = showFruit $ fromJust x

本质上 fromJust 有一个分支,它引发了 Error 所以如果我们尝试使用它,我们甚至不希望程序编译,因为 printFruit 绝对必须是"super"纯。例如,我们可以通过将行替换为:

来修复该问题
printFruit x = maybe "Unknown fruit!" (\y -> showFruit y) x

我觉得很奇怪 Haskell 决定实施严格的类型和不完整的模式检测,所有这些都是为了防止无效状态被表示的崇高追求,但却因为不给程序员提供而落在了终点线前一种在不允许时检测到 error 的分支的方法。从某种意义上说,这使得 Haskell 不如 Java 健壮,这迫使您声明允许您的函数引发的异常。

实现这个最简单的方法是以某种方式简单地取消定义error,通过某种形式的关联声明,在本地为一个函数和它的方程式使用的任何函数。然而,这似乎是不可能的。

The wiki page about errors vs exceptions 通过合同提到了一个名为 "Extended Static Checking" 的扩展,但它只会导致损坏 link.

基本上可以归结为:如何让上面的程序因为使用 fromJust 而无法编译?欢迎所有想法、建议和解决方案。

您的主要抱怨是关于 fromJust,但鉴于它从根本上违背了您的目标,您为什么要使用它?

记住 error 的最大原因是并不是所有的东西都可以按照类型系统可以保证的方式定义,所以 "This can't happen, trust me" 是有道理的。 fromJust 来自 "sometimes I know better than the compiler" 的这种心态。如果您不同意这种心态,请不要使用它。或者不要像这个例子那样滥用它。

编辑:

为了进一步阐述我的观点,想想你将如何实现你所要求的(关于使用部分函数的警告)。警告将在以下代码的什么地方应用?

a = fromJust $ Just 1
b = Just 2
c = fromJust $ c
d = fromJust
e = d $ Just 3
f = d b

这些毕竟都是静态不偏的。 (对不起,如果下一个的语法不对,那就晚了)

g x = fromJust x + 1
h x = g x + 1
i = h $ Just 3
j = h $ Nothing
k x y = if x > 0 then fromJust y else x
l = k 1 $ Just 2
m = k (-1) $ Just 3
n = k (-1) $ Nothing

i 这里又是安全的,但 j 不安全。哪个方法应该return警告?当这些方法中的一些或全部在我们的函数之外定义时,我们该怎么办。如果 k 是有条件的,你会怎么做?这些功能中的部分或全部应该失败吗?

通过让编译器进行此调用,您将混淆许多复杂的问题。尤其是当您考虑到仔细选择库可以避免这种情况时。

在任何一种情况下,IMO 解决这类问题的方法是在编译之后或期间使用工具来查找问题,而不是修改编译器。这样的工具可以更容易地理解 "your code" 与 "their code" 并更好地确定在不当使用时最好在哪里警告您。您还可以调整它而不必向源文件添加一堆注释以避免误报。我不知道什么工具,或者我会建议一个。

Haskell 允许任意的一般递归,因此如果只删除各种形式的 error,就好像所有 Haskell 程序都必然是完整的。也就是说,您可以只定义 error a = error a 来处理您不想处理的任何情况。 运行时间错误比无限循环更有帮助。

您不应将 error 视为与普通的 Java 异常相似。 error是断言失败,代表编程错误,像fromJust这样可以调用error的函数是断言。您不应该尝试捕获 error 产生的异常,除非在特殊情况下,例如即使请求的处理程序遇到编程错误,服务器也必须继续 运行。

答案是我想要的是一个整体检查水平,不幸的是 Haskell 没有提供(还?)。

或者我想要依赖类型(例如 Idris), or a static verifier (e.g. Liquid Haskell) or syntax lint detection (e.g. hlint)。

我现在真的在研究 Idris,它似乎是一种了不起的语言。这是我可以推荐观看的 talk by the founder of Idris

这些答案归功于@duplode、@chi、@user3237465 和@luqui。