Fork me on GitHub

转:Haskell Is Exceptionally Unsafe

It is well known that Haskell is not type safe. The most blatant violation is the all too necessary, but aptly named, unsafePerformIO operation. You are enjoined not to use this in an unsafe manner, and must be careful to ensure that the encapsulated computation may be executed at any time because of the inherent unpredictability of lazy evaluation. (As an aside, it is worth mentioning that the analogous operation in monadic ML, safePerformIO, is entirely safe, precisely because of the value restriction on polymorphism.) A less blatant violation is that the equational theory of the language with seq is different from the equational theory without it, and the last I knew the compiler was willing to make transformations that are valid only in the absence of this construct. This too is well-known. A proper reformulation of the equational theory was given by Patricia Johann a few years ago as a step towards solving it. (If the GHC compiler is no longer unsafe in this respect, it would be good to know.)

I’ve asked around a little bit, including some of the Haskell insiders, whether it is equally well known that the typed exception mechanism in Haskell is unsound. From what I can tell this seems not to be well-understood, so let me explain the situation as I see it, and I am sure I will be quickly corrected if I am wrong. The starting point for me was the realization that in Haskell pure code (outside of the IO monad) may raise an exception, and, importantly for my point, the exceptions are user-defined. Based on general semantic considerations, it seemed to me that this cannot possibly be sound, and Karl Crary helped me to isolate the source of the problem.

The difficulty is really nothing to do with exceptions per se, but rather with exception values. (So my point has nothing whatsoever to do with the concept of imprecise exceptions in Haskell.) It seems to me that the root cause is an all-too-common misunderstanding of the concept of typed exceptions as they occur, for example, in Standard ML. The mistake is a familiar one, the confusion of types with classes; it arises often in discussions related to oop. To clarify the situation let me begin with a few remarks exception values in ML, and then move on to the issue at hand.

The first point, which is not particularly relevant to the present discussion, is that exceptions have nothing to do with dynamic binding. The full details are in my book, so I will only summarize the situation here. Many seem to have the idea that an exception handler, which typically looks like a sequence of clauses consisting of an exception and an action, amounts to a dynamic binding of each exception to its associated handler. This is not so. In fact the left-hand side of an exception clause is a pattern, including a variable (of which a wild card is one example), or nested patterns of exceptions and values. I realize that one may implement the exception mechanism using a single dynamically bound symbol holding the current exception handler, but this implementation is but one of many possible ones, and does not in any case define the abstraction of exceptions. Exceptions have no more to do with dynamic binding than does the familiar if-then-else available in any language.

The second point, which is pertinent to this discussion, is that exceptions have only one type. You read that right: the typed exception mechanism in Standard ML is, in fact, uni-typed (where I have I heard that before?). It is perfectly true that in Standard ML one may associate a value of any type you like with an exception, and this value may be communicated to the handler of that exception. But it is perfectly false to say that there are multiple types of exceptions; there is only one, but it has many (in fact, “dynamically many”) classes. When you declare, say, exception Error of string in Standard ML you are introducing a new class of type string->exn, so that Error s is an exception value of type exn carrying a string. An exception handler associates to a computation of type α a function of type exn->α that handles any exceptions thrown by that computation. To propagate uncaught exceptions, the handler is implicitly post-composed with the handler fn x => raise x. Pattern matching recovers the type of the associated value in the branch that handles a particular exception. So, for example, a handler of the form Error x => exp propagates the fact that x has type string into the expression exp, and similarly for any other exception carrying any other type. (Incidentally, another perfectly good handler is Error “abcdef” => exp, which handles only an Error exception with associated value “abcdef”, and no other. This debunks the dynamic binding interpretation mentioned earlier.)

The third point is that exception classes are dynamically generated in the sense that each evaluation of an exception declaration generates a fresh exception. This is absolutely essential for modularity, and is extremely useful for a wide variety of other purposes, including managing information flow security within a program. (See Chapter 34 of my book for a fuller discussion.) O’Caml attempted to impose a static form of exceptions, but it is now recognized that this does not work properly in the presence of functors or separate compilation. This means that exception declarations are inherently stateful and cannot be regarded as pure.

This got me to wondering how Haskell could get away with user-defined typed exceptions in “pure” code. The answer seems to be “it can’t”, as the following example illustrates:

import Control.Exception

import Data.Typeable

newtype Foo = Foo (() -> IO ())

{- set Foo’s TypeRep to be the same as ErrorCall’s -}

instance Typeable Foo where

typeOf _ = typeOf (undefined :: ErrorCall)

instance Show Foo where show _ = “”

instance Exception Foo

main = Control.Exception.catch (error “kaboom”) ( (Foo f) -> f ())

If you run this code in GHC, you get “internal error: stg_ap_v_ret”, which amounts to “going wrong.” The use of exceptions is really incidental, except insofar as it forces the use of the class Typeable, which is exploited here.

How are we to understand this unsoundness? My own diagnosis is that typed exceptions are mis-implemented in Haskell using types, rather than classes. There is no need in ML for any form of type casting to implement exceptions, because there is exactly one type of exception values, albeit one with many classes. Haskell, on the other hand, tries to have exceptions with different types. This immediately involves us in type casting, and hilarity ensues.

The problem appears to be difficult to fix, because to do so would require that exception values be declared within the IO monad, which runs against the design principle (unavoidable, in my opinion) that exceptions are permissible in pure code. (Exceptions are, according to Aleks Nanevski, co-monadic, not monadic.) Alternatively, since Haskell lacks modularity, a possible fix is to permit only static exceptions in essentially the style proposed in O’Caml. This amounts to collecting up the exception declarations on a whole-program basis, and implicitly making a global data declaration that declares all the exception constructors “up front”. Exception handlers would then work by pattern matching, and all would be well. Why this is not done already is a mystery to me; the current strategy looks a lot like a kludge once you see the problem with safety.

Insofar as my interpretation of what is going on here is correct, the example once again calls into question the dogma of the separation of Church from state as it is realized in Haskell. As beguiling as it is, the idea, in its current form, simply does not work. Dave MacQueen recently described it to me as a “tar baby”, from the Brer Rabbit story: the more you get involved with it, the more entangled you become.

Submitted by at 6 years ago



puffy 6 years ago