【问题标题】:Robust haskell without errors没有错误的健壮的haskell
【发布时间】:2015-08-08 00:06:10
【问题描述】:

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

相反,我指的是由错误状态引起的错误,我想通过严格的静态类型使这些状态不可表示和不可编译(在某些函数中)来消除这些错误。在我看来,我将这些函数称为“纯”函数,并认为强类型系统可以让我完成这一任务。然而 Haskell 并没有以这种方式定义“纯”,并允许程序在任何情况下通过 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 绝对必须是“超级”纯的。我们可以解决这个问题,例如将行替换为:

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

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

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

The wiki page about errors vs exceptions 为此通过合同提到了一个名为“扩展静态检查”的扩展,但这只会导致链接断开。

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

【问题讨论】:

  • (1) 我相信these are the papers you are looking for。 (2) 注意printFruit 是不必要的。如果用户想要显示Maybe Fruit,她可以使用fmap showFruit,然后以合理的方式打开Maybe。这当然不能回答你的问题(最好让用户远离fromJust!),所以继续吧:)
  • 我希望 Haskell 有一种整体检查器。我什至对无异常检查器检查非穷举性和部分函数的使用感到满意,但不检查无限循环。我也想把这些警告变成错误,这样我就不能轻易忽视它们。在 GHC 中实施这可能不需要大量的工作。
  • 你看过伊德里斯吗?它可能是您想要的语言。
  • 另见hlint

标签: haskell robustness


【解决方案1】:

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

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

【讨论】:

  • @hanneslandeholm,你应该考虑像 Coq 和 Agda 这样的语言。 Haskell 不是为你想要的而设计的,尽管它提供了一些很好的工具来编写安全和正确的代码。到目前为止,还没有人设法编写出一种也适用于大多数实际编程的依赖类型、强规范化语言。 Haskell 是一种妥协。
  • @dfeuer,为什么 Idris 不适合实际编程?它有适当的效果系统,编译成 C,执行各种优化,有大量的语法糖,适当的类型漏洞(嗯,Agda 更好),战术和其他非常实用的东西。将此与超级丑陋的单子变压器和hasochism 的研究所进行比较。一种实用的依赖类型编程语言已经存在,它只是拥有纯粹的生态系统。
  • @user3237465,在我看来,Idris 感觉更像是一桶螺栓,而不是工具包。试图证明其中的重要定理非常困难,并且首席开发人员似乎并不太关心该应用程序。在尝试编写经过形式验证的高效数据结构而又不费吹灰之力时,惰性非常重要。但是 Idris 的自动延迟/强制插入在证明方面非常糟糕。也许伊德里斯有一天会好起来,但现在还没有。
  • @dfeuer,我同意,Idris 不是很好的证明助手(底层理论不包含元变量,emacs 模式非常笨拙,没有 unicode 和 mixfix 运算符等),但它是仍然是一门很棒的编程语言。
  • @user3237465,一旦您在 Idris 中编写了 Van Laarhoven 镜头,您会发现它们很难使用。一旦你根据VerifiedSemigroupMonoid 定义了VerifiedMonoid,你会发现它很难使用。很容易对 Haskell 的全局实例一致性嗤之以鼻,直到你看到没有它的生活有多艰难以及它所支持的解决方法。请问为什么在基础理论中有元变量是件好事?
【解决方案2】:

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

【讨论】:

  • 你从错误的角度接近我的问题。该示例旨在强调程序员如何错误地引入错误路径。这个错误也可能更微妙,比如允许一个可能被零除的分支。 “好吧,只是不要犯错误”的假设使类型系统本身在很大程度上毫无意义。此外,您承认该函数更危险,如果编译器可以以某种方式阻止对此类危险函数的调用,那不是很有用吗?
  • @HannesLandeholm:我会扩展我的答案,但这不是“不要犯错误”的情况,而是“不要使用不符合您的范式的工具”。如果fromJust 和它的同类可以破坏保证,那么你的论点是有道理的,但是_|_ 总是可以爬上去的。
  • 我最近开始研究一个 700 万行代码库(不在 Haskell 中),所以我在阅读 OP 的问题时戴上了这个镜头。在这么大的地方,“不要使用 X”并不是一个真正可以接受的解决方案。对于此类情况,必须进行工具检查。如果我没记错的话,FWIW hlint 会这样做。
  • @luqui:hlint 是我想说的应该解决方案。编译器不需要也很可能不应该是所有工程问题的解决方案。
【解决方案3】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 2010-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多