8

我目前正在学习 Haskell。我选择这种语言的动机之一是编写具有高度鲁棒性的软件,即完全定义的、数学上确定的、永远不会崩溃或产生错误的函数。我并不是指由系统谓词(“系统内存不足”、“计算机着火”等)引起的故障,这些并不有趣,并且可以简单地使整个过程崩溃。我也不是指由无效声明(pi = 4)引起的错误行为。

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

为什么捕获异常是非纯的,但抛出异常是纯的?

这是完全可以接受的,一点也不奇怪。然而令人失望的是,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

假设我偏执于纯函数readFruit并且printFruit真的不会由于非手动状态而失败。你可以想象代码是用于发射一个装满宇航员的火箭,在一个绝对关键的例程中需要序列化和反序列化水果值。

第一个危险自然是我们在模式匹配中犯了一个错误,因为这给了我们无法处理的可怕错误状态。值得庆幸的是,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绝对必须是“超级”纯的。我们可以解决这个问题,例如将行替换为:

printFruit x = maybe "Unknown fruit!" (\y -> showFruit y) x

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

实现这一点的最简单的方法error是以某种方式简单地 undefined,通过某种形式的关联声明,在本地为函数及其方程使用的任何函数。然而,这似乎是不可能的。

关于错误与异常的 wiki 页面为此通过合同提到了一个名为“扩展静态检查”的扩展,但它只会导致链接断开。

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

4

3 回答 3

4

Haskell 允许任意的一般递归,所以并不是所有的 Haskell 程序都必须是全部的,只要error它的各种形式被删除。也就是说,您可以定义error a = error a处理任何您不想处理的情况。运行时错误比无限循环更有帮助。

您不应该认为error它类似于普通的 Java 异常。error是表示编程错误的断言失败fromJust,可以调用的函数error是断言。您不应该尝试捕获由errorexcept 在特殊情况下产生的异常,例如服务器必须继续运行,即使请求的处理程序遇到编程错误也是如此。

于 2015-08-08T01:35:36.733 回答
1

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

请记住,最大的原因error是并非所有内容都可以以类型系统可以保证的方式定义,因此“这不可能发生,相信我”是有道理的。fromJust来自“有时我比编译器更了解”的这种心态。如果您不同意这种心态,请不要使用它。或者不要像这个例子那样滥用它。

编辑:

为了扩展我的观点,想想你将如何实现你所要求的(使用部分函数的警告)。警告将在以下代码中适用于何处?

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不是。哪个方法应该返回警告?当这些方法中的一些或全部在我们的函数之外定义时,我们该怎么办。k在有条件的部分情况下你会怎么做?这些功能中的一部分或全部是否应该失败?

通过使编译器进行此调用,您将许多复杂的问题混为一谈。尤其是当您考虑仔细选择库以避免它时。

在这两种情况下,IMO 对这类问题的解决方案是在编译之后或编译期间使用工具来查找问题,而不是修改编译器。这样的工具可以更容易地理解“您的代码”与“他们的代码”,并更好地确定在不当使用时最好的警告位置。您也可以对其进行调整,而无需在源文件中添加大量注释以避免误报。我不知道一种工具,或者我会推荐一个。

于 2015-08-08T00:17:33.217 回答
1

答案是我想要的是 Haskell 不幸没有提供的整体检查级别(还没有?)。

或者,我想要依赖类型(例如Idris),或静态验证器(例如Liquid Haskell)或语法 lint 检测(例如hlint)。

我现在真的在研究 Idris,它似乎是一种了不起的语言。这是Idris 创始人的演讲,我可以推荐观看。

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

于 2015-08-09T15:26:08.620 回答