我一直想知道Haskell异常系统如何适应整个“纯函数式语言”的东西。例如,请参阅下面的GHCi会话。
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Prelude> head []
*** Exception: Prelude.head: empty list
Prelude> :t head
head :: [a] -> a
Prelude> :t error
error :: [Char] -> a
Prelude> error "ranch"
*** Exception: ranch
CallStack (from HasCallStack):
error, called at <interactive>:4:1 in interactive:Ghci1
Prelude>
头部的类型是[a] - &gt;一个。但是当你在空列表的特殊情况下调用它时,你会得到一个例外。但是类型签名中没有考虑这个例外。
如果我没记错的话,在模式匹配过程中出现故障时,情况类似。类型签名所说的并不重要,如果你没有考虑到所有可能的模式,你就有可能抛出异常。
我没有一个简明扼要的问题要问,但我的头脑正在游泳。将这个奇怪的异常系统添加到其他纯粹优雅语言的动机是什么?它仍然是纯净的,但我只是缺少一些东西?如果我想利用这个异常功能,我将如何去做(比如我如何捕获和处理异常?还有什么我可以用它们做的吗?)例如,如果我编写使用它的代码“头”功能,当然我应该采取预防措施,以便空列表以某种方式走私自己。
答案 0 :(得分:11)
您混淆了两个概念:纯度和整体。
Haskell 是纯,但是不是总数。
在IO
之外,非终止(例如,let loop = loop in loop
)和例外(例如,error "urk!"
)是相同的 - 非终止和例外条款,当被强制时,不评估为值。 Haskell的设计者想要一种图灵完备的语言 - 根据停止问题 - 意味着它们总体上是完整的。一旦你有了不确定性,我想你也可以有例外 - 定义error msg = error msg
并且调用error
永远不做任何事情在实践中比实际更不令人满意在有限的时间内看到你想要的错误信息!
一般而言,你是对的 - 部分函数(那些没有为每个输入值定义的函数,如head
)都是丑陋的。现代Haskell通常更喜欢通过返回Maybe
或Either
值来编写总函数,例如
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
errHead :: [a] -> Either String a
errHead [] = Left "Prelude.head: empty list"
errHead (x:_) = Right x
在这种情况下,Functor
,Applicative
,Monad
,MonadError
,Foldable
,Traversable
等机制会合并这些全部功能和使用结果很容易。
如果您在代码中遇到异常 - 例如,您可以使用error
检查您认为已执行的代码中的复杂不变量,但是您有一个错误 - 您可以捕获它在IO
。这回到了为什么可以与IO
中的异常进行交互的问题 - 这不会使语言不纯吗?答案与我们为什么可以在IO
中执行I / O或使用可变变量的问题相同 - 评估类型IO A
的值不会产生副作用描述,它只是一个描述程序可以做什么的动作。 (互联网上的其他地方有更好的描述;例外与其他效果没有任何不同。)
(另请注意,有a separate-but-related exception system in IO
,例如在尝试读取不存在的文件时使用。人们通常可以使用此异常系统,但是因为你在IO
您已使用不纯的代码。)
答案 1 :(得分:4)
例如,如果我编写使用“head”函数的代码,我当然应该采取预防措施来处理空列表以某种方式走私自己的情况。
更简单的解决方案:不要使用head
。有很多替换:listToMaybe
Data.Maybe
,safe包中的各种替代实现,等等。基本库中的部分函数[1] - 特别容易替换为head
- 仅仅是历史遗留物,应该被忽略或替换为安全变体,例如前面提到的 safe 包中的变体。有关进一步的论点,here is an entirely reasonable rant about partial functions。
如果我想利用这个异常功能,我将如何去做(即如何捕捉和处理异常?还有什么我可以用它们做的吗?)
error
抛出的排序异常只能在IO
monad中捕获。如果您正在编写纯函数,则不希望强制用户仅在IO
monad中运行它们以捕获异常。因此,如果您在纯函数中使用error
,则假设不会捕获错误[2]。理想情况下,您根本不应在纯代码中使用error
,但如果您不得不这样做,请至少确保编写一条信息性错误消息(即 not “ Prelude.head:空列表“),以便用户知道程序崩溃时发生了什么。
如果我没记错的话,在模式匹配过程中出现故障时,情况类似。类型签名所说的并不重要,如果你没有考虑到所有可能的模式,你就有可能抛出异常。
事实上。与使用head
明确写出不完整模式匹配(\(x:_) -> x)
的唯一区别在于,在后一种情况下,如果使用-Wall
,编译器将至少警告您,而head
1}}即便是在地毯下扫过。
我一直想知道Haskell异常系统如何适应整个“纯函数式语言”。
从技术上讲,部分功能不影响纯度(当然,这并不会使它们变得不那么令人讨厌)。从理论的角度来看,head []
与foo = let x = x in x
之类的东西一样未定义。 (进一步阅读这些细微之处的关键字是"bottom"。)
[1]:部分函数是函数,就像head
一样,没有为它们应该采用的参数类型的某些值定义。
[2]:值得一提的是IO
中的例外是一个完全不同的问题,因为你不能轻易避免例如仅通过使用更好的函数来读取文件失败。以合理的方式处理这些场景有很多方法。如果您对此问题感到好奇,here is one "highly opinionated" article about it就是相关工具和权衡的说明。
答案 2 :(得分:2)
Haskell并不要求您的功能是完整的,并且不会跟踪它们何时不是。 (总函数是那些为其输入类型的每个可能值都有明确定义的输出的函数)
即使没有异常或模式匹配失败,您也可以拥有一个不会为某些输入定义输出的函数。一个例子是length (repeat 1)
。这将继续永远计算,但从未实际抛出错误。
Haskell语义“应对”的方式是声明每个类型都有一个“额外”值;所谓的“bottom value”,并声明任何未正确完成并生成其类型的正常值的计算实际上会产生最低值。它由数学符号⊥表示(仅在谈论关于 Haskell时;在Haskell中没有任何方法可以直接引用此值,但是undefined
通常也被使用,因为< em>是一个绑定到错误计算计算的Haskell名称,因此在语义上产生底部值)。
这个 是系统中的一个理论疣,因为它让你能够创建任何类型的“值”(尽管不是非常有用的),以及很多关于它的推理基于类型正确的代码位实际上依赖于你不能做到这一点的假设(如果你进入了纯函数程序和形式逻辑之间的Curry-Howard同构,存在⊥使您能够“证明”逻辑矛盾,从而完全证明任何事情。
但是在实践中似乎可以看出,通过假装⊥在Haskell中不存在所做的所有推理通常仍然能够很好地工作,当你编写“行为良好”的代码时不能使用⊥非常。
在Haskell中容忍这种情况的主要原因是作为编程语言而不是形式逻辑或数学系统的易用性。不可能使编译器能够实际告诉任何类似Haskell的代码,无论每个函数是全部还是部分(参见暂停问题)。因此,一种想要强制执行整体性的语言必须要么删除许多你可以做的事情,要么要求你跳过很多箍来证明你的代码总是终止,或者两者兼而有之。 Haskell设计师不想这样做。
因此,考虑到Haskell作为一种语言已经转向偏袒和⊥,它也可以为您提供error
之类的便利。毕竟,你总是可以通过不终止来编写error :: String -> a
函数;立即打印出错误消息而不是让程序永远旋转对于练习程序员来说更有用,即使这些在Haskell语义理论中都是等价的! p>
类似地,Haskell的原始设计者决定隐式地为每个模式匹配添加一个包含所有模式匹配的情况,只是错误输出比强制程序员每次期望他们的代码的一部分显式添加错误情况更方便只看到某些情况。 (尽管包括我在内的很多Haskell程序员都使用了不完整模式匹配警告,并且几乎总是将其视为错误并修复他们的代码,因此可能更喜欢原始的Haskell设计者在这一方面采取另一种方式)
<强> TLDR 强>; error
中的异常和模式匹配失败是为了方便起见,因为它们不会使系统比以前更糟糕,而不是与Haskell完全不同的系统。
可以通过使用Control.Exception中的设施,通过抛出和捕获异常来实现编程,包括从 为了不破坏系统的纯度,您可以从任何地方提出异常(因为系统总是要处理函数无法正确终止和生成值的可能性;“提高异常“只是可能发生的另一种方式”,但异常只能由 我根本没有使用过这个(通常我更喜欢使用Haskell的语义模型中比error
或模式匹配失败中捕获异常。 / p>
IO
中的构造捕获。因为IO
的形式语义基本上允许发生任何事情(因为它必须与现实世界接口,并且我们可以从Haskell的定义中实际上没有任何硬限制),我们也可以放松我们在Haskell中对纯函数通常需要的大多数规则,并且仍然具有技术上适合Haskell代码纯模型的东西。IO
的操作模型更明确定义的东西来保持我的错误处理,可以像Maybe
或Either
一样简单,但如果您愿意,可以阅读它。