简体   繁体   English

“错误”功能的存在如何影响Haskell的纯度?

[英]How does the presence of the “error” function bear on the purity of Haskell?

I've always wondered how the Haskell exception system fits in with the whole "Pure functional language" thing. 我一直想知道Haskell异常系统如何适应整个“纯函数式语言”的东西。 For example see the below GHCi session. 例如,请参阅下面的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>

The type of head is [a] -> a. 头的类型是[a] - > a。 But when you call it on the special case of an empty list, you get an exception instead. 但是当你在空列表的特殊情况下调用它时,你会得到一个例外。 But this exception is not accounted for in the type signature. 但是类型签名中没有考虑此异常。

If I remember correctly it's a similar story when there is a failure during pattern matching. 如果我没记错的话,在模式匹配过程中出现故障时,情况类似。 It doesn't matter what the type signature says, if you haven't accounted for every possible pattern, you run the risk of throwing an exception. 类型签名所说的并不重要,如果你没有考虑到每种可能的模式,你就有可能抛出异常。

I don't have a single, concise question to ask, but my head is swimming. 我没有一个简明扼要的问题要问,但我的头脑正在游泳。 What was the motivation for adding this strange exception system to an otherwise pure and elegant language? 将这个奇怪的异常系统添加到其他纯粹优雅语言的动机是什么? Is it still pure but I'm just missing something? 它仍然是纯净的,但我只是缺少一些东西? If I want to take advantage of this exception feature, how would I go about doing it (ie how do I catch and handle exceptions? is there anything else I can do with them?) For example, if ever I write code that uses the "head" function, surely I should take precautions for the case where an empty list somehow smuggles itself in. 如果我想利用这个异常功能,我将如何去做(比如我如何捕获和处理异常?还有什么我可以用它们做的吗?)例如,如果我编写使用它的代码“头”功能,当然我应该采取预防措施,以一个空列表以某种方式走私自己的情况。

You are confusing two concepts: purity and totality . 你混淆了两个概念: 纯度总体

  • Purity says that functions have no side effects. Purity说功能没有副作用。
  • Totality says that every function terminates and produces a value. Totality说每个函数都终止并产生一个值。

Haskell is pure, but is not total. Haskell 纯粹,但并不完全。

Outside of IO , nontermination (eg, let loop = loop in loop ) and exceptions (eg, error "urk!" ) are the same – nonterminating and exceptional terms, when forced, do not evaluate to a value. IO之外,nontermination(例如, let loop = loop in loop )和异常(例如, error "urk!" )是相同的 - 非强制和异常术语,当被强制时,不评估为值。 The designers of Haskell wanted a Turing-complete language, which – as per the halting problem – means that they forwent totality. Haskell的设计者想要一种图灵完备的语言 - 根据停止问题 - 意味着它们总体上是完整的。 And once you have nontermination, I suppose you might as well have exceptions, too – defining error msg = error msg and having calls to error do nothing forever is much less satisfying in practice than actually seeing the error message you want in finite time! 一旦你有了无限制,我想你也可能也有异常 - 定义error msg = error msg并且error调用永远没有做任何事情在实践中比在有限时间内实际看到你想要的错误消息更不令人满意!

In general, though, you're right – partial functions (those which are not defined for every input value, like head ) are ugly. 但是,一般情况下,你是对的 - 部分函数(那些没有为每个输入值定义的函数,比如head )都是丑陋的。 Modern Haskell generally prefers writing total functions instead by returning Maybe or Either values, eg 现代Haskell通常更喜欢通过返回MaybeEither值来编写总函数,例如

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

In this case, the Functor , Applicative , Monad , MonadError , Foldable , Traversable , etc., machinery makes combining these total functions and working with their results easy. 在这种情况下, FunctorApplicativeMonadMonadErrorFoldableTraversable等机械结合了这些总功能并轻松地使用它们的结果。

Should you actually come across an exception in your code – for instance, you might use error to check a complicated invariant in your code that you think you've enforced, but you have a bug – you can catch it in IO . 您是否真的在代码中遇到异常 - 例如,您可能会使用error来检查您认为已执行的代码中的复杂不变量,但是您有一个错误 - 您可以在IO捕获它。 Which returns to the question of why it's OK to interact with exceptions in IO – doesn't that make the language impure? 这回到了为什么可以与IO异常进行交互的问题 - 这是否会使语言不纯? The answer is the same as that to the question of why we can do I/O in IO , or work with mutable variables – evaluating a value of type IO A doesn't produce the side effects that it describes, it's just an action that describes what a program could do. 答案与我们为什么可以在IO执行I / O或使用可变变量的问题相同 - 评估IO A类型的值不会产生它描述的副作用,它只是一个动作,描述程序可以做什么。 (There are better descriptions of this elsewhere on the internet; exceptions aren't any different than other effects.) (互联网上的其他地方有更好的描述;例外与其他效果没有任何不同。)

(Also, note that there is a separate-but-related exception system in IO , which is used when eg trying to read a file that isn't there. People are often OK with this exception system, in moderation, because since you're in IO you're already working with impure code.) (另请注意, IO中有一个单独但相关的异常系统例如在尝试读取不存在的文件时使用。人们通常可以使用此异常系统,但是因为你'在IO你已经使用了不纯的代码。)

For example, if ever I write code that uses the "head" function, surely I should take precautions for the case where an empty list somehow smuggles itself in. 例如,如果我编写使用“head”函数的代码,我肯定应该采取预防措施来处理空列表以某种方式走私自己的情况。

A simpler solution: don't use head . 一个更简单的解决方案:不要使用head There are plenty of replacements: listToMaybe from Data.Maybe , the various alternative implementations in the safe package, etc. The partial functions [1] in the base libraries -- specially ones as easy to replace as head -- are little more than historical cruft, and should be either ignored or replaced by safe variants, such as those in the aforementioned safe package. 有很多替代的: listToMaybeData.Maybe ,在各种不同的备选实施方式中的安全包等部分功能[1]在基库-特别如那些易于更换为head -比历史更小应该忽略或替换为安全的变体,例如上述安全包装中的安全变体。 For further arguments, here is an entirely reasonable rant about partial functions . 对于进一步的论证, 这是一个关于部分函数的完全合理的咆哮

If I want to take advantage of this exception feature, how would I go about doing it (ie how do I catch and handle exceptions? is there anything else I can do with them?) 如果我想利用这个异常功能,我将如何去做(即如何捕获和处理异常?还有什么我可以用它们做的吗?)

Exceptions of the sort thrown by error can only be caught in the IO monad. error引发的排序异常只能在IO monad中捕获。 If you are writing pure functions you won't want to force your users to run them in the IO monad merely for catching exceptions. 如果您正在编写纯函数,则不希望强制用户仅在IO monad中运行它们以捕获异常。 Therefore, if you ever use error in a pure function, assume the error will not be caught [2]. 因此,如果您在纯函数中使用error ,则假定错误不会被捕获[2]。 Ideally you shouldn't use error in pure code at all, but if you are somehow compelled to do so, at least make sure to write an informative error message (that is, not "Prelude.head: empty list") so that your users know what is going on when the program crashes. 理想情况下,您根本不应该在纯代码中使用error ,但如果您不知何故必须这样做,请至少确保编写一条信息性错误消息(即不是 “Prelude.head:empty list”),以便您的用户知道程序崩溃时发生了什么。

If I remember correctly it's a similar story when there is a failure during pattern matching. 如果我没记错的话,在模式匹配过程中出现故障时,情况类似。 It doesn't matter what the type signature says, if you haven't accounted for every possible pattern, you run the risk of throwing an exception. 类型签名所说的并不重要,如果你没有考虑到每种可能的模式,你就有可能抛出异常。

Indeed. 确实。 The only difference from using head to writing the incomplete pattern match (\\(x:_) -> x) by yourself explicitly is that in the latter case the compiler will at least warn you if you use -Wall , while with head even that is swept under the rug. 唯一的区别就是使用head来自己编写不完整的模式匹配(\\(x:_) -> x) ,在后一种情况下编译器至少会警告你,如果你使用-Wall ,而head甚至是在地毯下扫过。

I've always wondered how the Haskell exception system fits in with the whole "Pure functional language" thing. 我一直想知道Haskell异常系统如何适应整个“纯函数式语言”的东西。

Technically speaking, partial functions don't affect purity (which doesn't make them any less nasty, of course). 从技术上讲,部分功能不会影响纯度(当然,这并不会使它们变得不那么令人讨厌)。 From a theoretical point of view, head [] is just as undefined as things like foo = let x = x in x . 从理论的角度来看, head []foo = let x = x in x类的东西一样未定义。 (The keyword for further reading into such subtleties is "bottom" .) (进一步阅读这些微妙之处的关键词是“底部” 。)


[1]: Partial functions are functions that, just like head , are not defined for some values of the argument types they are supposed to take. [1]:部分函数是函数,就像head一样,没有为它们应该采用的参数类型的某些值定义。

[2]: It is worth mentioning that exceptions in IO are a whole different issue, as you can't trivially avoid eg a file read failure just by using better functions. [2]:值得一提的是IO中的异常是一个完全不同的问题,因为你不能仅仅通过使用更好的函数来避免例如文件读取失败。 There are quite a few approaches towards handling such scenarios in a sensible way. 以合理的方式处理这些场景有很多方法。 If you are curious about the issue, here is one "highly opinionated" article about it that is illustrative of the relevant tools and trade-offs. 如果你对这个问题感到好奇, 这里有一篇关于它的“高度自以为是”的文章说明了相关的工具和权衡。

Haskell does not require that your functions be total, and doesn't track when they're not. Haskell不要求你的函数是完整的,也不要追踪它们何时不是。 (Total functions are those that have a well defined output for every possible value of their input type) (总函数是那些为其输入类型的每个可能值都有明确定义的输出的函数)

Even without exceptions or pattern match failures, you can have a function that doesn't define output for some inputs by just going on forever. 即使没有异常或模式匹配失败,您也可以拥有一个函数,通过永远继续,不会为某些输入定义输出。 An example is length (repeat 1) . 一个例子是length (repeat 1) This continues to compute forever, but never actually throws an error. 这将继续永远计算,但从未实际抛出错误。

The way Haskell semantics "copes" with this is to declare that there is an "extra" value in every single type; Haskell语义“应对”的方式是声明每个类型都有一个“额外”值; the so called " bottom value ", and declare that any computation that doesn't properly complete and produce a normal value of its type actually produces the bottom value. 所谓的“ 底值 ”,并声明任何未正确完成并产生其类型的正常值的计算实际上产生底值。 It's represented by the mathematical symbol ⊥ (only when talking about Haskell; there isn't really any way in Haskell to directly refer to this value, but undefined is often also used since that is a Haskell name that is bound to an error-raising computation, and so semantically produces the bottom value). 它是由数学符号⊥(仅当谈到 Haskell的代表;有没有真正在Haskell任何方式直接引用此值,但是undefined也经常使用,因为这绑定到一个错误认识一个Haskell名计算,从而在语义上产生底值)。

This is a theoretical wart in the system, since it gives you the ability to create a 'value' of any type (albeit not a very useful one), and a lot of the reasoning about bits of code being correct based on types actually relies on the assumption that you can't do exactly that (if you're into the Curry-Howard isomorphism between pure functional programs and formal logic, the existence of ⊥ gives you the ability to "prove" logical contradictions, and thus to prove absolutely anything at all). 是系统中的理论瑕疵,因为它使您能够创建任何类型的“值”(虽然不是非常有用的),并且基于类型实际依赖的代码位数的许多推理假设你不能做到这一点(如果你进入纯函数程序和形式逻辑之间的Curry-Howard同构,⊥的存在使你有能力“证明”逻辑矛盾,从而绝对证明什么都没有)。

But in practice it seems to work out that all the reasoning done by pretending that ⊥ doesn't exist in Haskell still generally works well enough to be useful when you're writing "well-behaved" code that doesn't use ⊥ very much. 但是在实践中似乎可以解决所有通过假装⊥在Haskell中不存在所做的推理通常仍然能够很好地工作,当你编写“表现良好”的代码时,它不会非常使用⊥ 。

The main reason for tolerating this situation in Haskell is ease-of-use as a programming language rather than a system of formal logic or mathematics. 在Haskell中容忍这种情况的主要原因是易于使用作为编程语言而不是形式逻辑或数学系统。 It's impossible to make a compiler that could actually tell of arbitrary Haskell-like code whether or not each function is total or partial (see the Halting Problem). 不可能使编译器能够实际告诉任何类似Haskell的代码,无论每个函数是全部还是部分(参见暂停问题)。 So a language that wanted to enforce totality would have to either remove a lot of the things you can do, or require you to jump through lots of hoops to demonstrate that your code always terminates, or both. 因此,一种想要强制执行整体性的语言必须要么删除许多你可以做的事情,要么要求你跳过很多箍来证明你的代码总是终止,或者两者兼而有之。 The Haskell designers didn't want to do that. Haskell设计师不想这样做。

So given that Haskell as a language is resigned to partiality and ⊥, it may as well give you things like error as a convenience. 因此,考虑到Haskell作为一种语言被置于偏袒和⊥,它也可能会给你带来像error这样的东西作为一种便利。 After all, you could always write a error :: String -> a function by just not terminating; 毕竟,你总是可以通过不终止来写一个error :: String -> a函数; getting an immediate printout of the error message rather than having the program just spin forever is a lot more useful to practicing programmers, even if those are both equivalent in the theory of Haskell semantics! 立即打印出错误消息而不是让程序永远旋转对于练习程序员来说更有用,即使这些在Haskell语义理论中都是等价的!

Similarly, the original designers of Haskell decided that implicitly adding a catch-all case to every pattern match that just errors out would be more convenient than forcing programmers to add the error case explicitly every time they expect a part of their code to only ever see certain cases. 类似地,Haskell的原始设计者决定隐式地为每个模式匹配添加一个包含所有模式匹配的情况,只是错误输出比强制程序员每次期望他们的代码的一部分只看到它们时明确地添加错误情况更方便某些情况。 (Although a lot of Haskell programmers, including me, work with the incomplete-pattern-match warning and almost always treat it as an error and fix their code, and so would probably prefer the original Haskell designers went the other way on this one). (尽管包括我在内的很多Haskell程序员都使用了不完整模式匹配警告,并且几乎总是将其视为错误并修复他们的代码,因此可能更喜欢原始的Haskell设计者在这一方面采取另一种方式) 。

TLDR ; TLDR ; exceptions from error and pattern match failure are there for convenience, because they don't make the system any more broken than it already has to be, without being quite a different system than Haskell. error和模式匹配失败的例外是为了方便起见,因为它们不会使系统破碎而不是已经存在,而不是与Haskell完全不同的系统。


You can program by throwing and catch exceptions if you really want, including catching the exceptions from error or pattern match failure, by using the facilities from Control.Exception . 可以通过使用来自Control.Exception的工具,通过抛出和捕获异常进行编程,包括从error或模式匹配失败中捕获异常。

In order to not break the purity of the system you can raise exceptions from anywhere (because the system always has to deal with the possibility of a function not properly terminating and producing a value; "raising an exception" is just another way in which that can happen), but exceptions can only be caught by constructs in IO . 为了不破坏系统的纯度,你可以从任何地方引发异常(因为系统总是必须处理函数不能正确终止和产生值的可能性;“引发异常”只是另一种方式可能发生),但异常只能通过IO的构造捕获。 Because the formal semantics of IO permit basically anything to happen (because it has to interface with the real world and there aren't really any hard restrictions we can impose on that from the definition of Haskell), we can also relax most of the rules we usually need for pure functions in Haskell and still have something that technically fits in the model of Haskell code being pure. 因为IO的形式语义基本上允许发生任何事情(因为它必须与现实世界接口,并且我们可以从Haskell的定义中实施任何硬限制),我们也可以放宽大多数规则我们通常需要Haskell中的纯函数,并且仍然具有技术上适合Haskell代码纯模型的东西。

I haven't used this very much at all (usually I prefer to keep my error handling using things that are more well-defined in terms of Haskell's semantic model than the operational model of what IO does, which can be as simple as Maybe or Either ), but you can read about it if you want to. 我根本没有使用过这个(通常我更喜欢使用Haskell的语义模型中定义比IO操作模型更明确的东西来保持我的错误处理,这可以像Maybe或者简单一样简单。 Either ),但如果你愿意,你可以阅读它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM