Hi everyone, I've been using Haskell for 1-2 months now, and feel fairly comfortable with the language. However, my #1 gripe is the difficulty of working with exceptions. I have two main complaints: difficulty of defining custom exceptions, and difficulty of handling exceptions. I've been working on a library of various bits of Haskell code. There are several things that could raise exceptions within that library. There's no real easy way to do it, and make it easy for people using the library to trap only my exceptions. I could just use error("foo: bar") and tell them to look for a string that starts with "foo:". It works, but it's clumsy, and unreliable too ("foo:" is a valid filename, for instance, and could be used in other exceptions.) My other choice is to use Dynamic for my exceptions, but that makes it even more difficulty to catch and handle, and requires the programmer to be ready to deal with a fairly esoteric part of Haskell. In OCaml, there is an exception keyword, that is essentially the same as a data keyword. It defines a new exception, and can use the same constructors, etc. that any other type can. It's very useful. In Python, the situation is even better. There, an object can be thrown as an exception. Moreover, when catching exceptions, you can match an exception by a particular object *or any of its parents*. That means that I can easily extend someone else's work, adding my own exceptions as child objects of the existing ones. When I want to catch exceptions, I can be as specific or as general as I want, and any code designed to catch the more general exceptions will keep working. Java also works this way. Now, on to catching errors. OCaml makes this easy: try (whatever) with End_of_file -> foo | My_custom_error x -> bar x Here, "whatever", "foo", and "bar x" must all be of the same type. If there is no error, whatever is returned. If there is an error that we catch, the given value is returned. Otherwise, the exception is passed on up. Python can work that way, but also adds another feature: try: blah moreblah finally: foo The code "foo" will be executed whenever an exception occurs in the try block, or whenever the block ends normally. In other words, it is *always* executed. This is useful for doing cleanup actions like closing network connections. Haskell's exception catching doesn't really have less functionality than OCaml (some might argue it has more), but it takes a lot more effort to compose, needing to provide a function that returns a function in many cases. The other annoying thing is forcing it to run in the IO monad. I don't really understand this restriction. If we're not dealing with IO code to start with, we have deterministic behavior (if something raises an exception on a given value once, it will do that every time). So why do we have: catchJust :: (Exception -> Maybe b) -> IO a -> (b -> IO a) -> IO a instead of: catchJust :: (Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a This would let us run an arbitrary function (so long as it takes one parameter) and handle the exception completely outside of the IO monad. I can't think of a theoretical reason that this wouldn't work, though I suppose I may be missing something. Thoughts? Flames? :-) -- John
The other annoying thing is forcing it to run in the IO monad.
necessarily so, since Haskell has non-strict semantics so it's not so clear when an exception is actually raised (you might have left the block that textually contained the offending expression , and the exception handler, a long time ago) -- -- Johannes Waldmann, Tel/Fax: (0341) 3076 6479 / 6480 -- ------ http://www.imn.htwk-leipzig.de/~waldmann/ ---------
On Tue, Nov 23, 2004 at 04:12:52PM +0100, Johannes Waldmann wrote:
The other annoying thing is forcing it to run in the IO monad.
necessarily so, since Haskell has non-strict semantics so it's not so clear when an exception is actually raised (you might have left the block that textually contained the offending expression , and the exception handler, a long time ago)
I'm not sure I follow that. Let's say I have a function: myfunc :: String -> Int This does some sort of string parsing and returns an Int. Or it may raise an exception if it couldn't parse the string. But it would do that every time. Now, let's say we have a non-IO catchJust. Of course, if we never need the value, we never run the function -- or catchJust. If we do need the value, we run the function in the context of catchJust. If it raises our exception, catchJust handles it and returns some default. If it raises no exception, it's the same as having no handler at all. And if it raises some other exception, it's also the same as having no handler at all. So what am I missing here?
I am sure this discussion has happened before, but I think for pure functions, returning Either Error Result is the way to go. Keean. John Goerzen wrote:
On Tue, Nov 23, 2004 at 04:12:52PM +0100, Johannes Waldmann wrote:
The other annoying thing is forcing it to run in the IO monad.
On Tue, Nov 23, 2004 at 04:30:21PM +0000, Keean Schupke wrote:
I am sure this discussion has happened before, but I think for pure functions, returning Either Error Result is the way to go.
That's certainly possible, but extremely tedious. One example: I've written an FTP client library. For every operation, there are several possible outcomes... mainly: success, low-level network error, or server error. Having to pick apart an Either from every command to CD, set transfer types, etc. would get so tedious that the code would, I think, become spaghetti fast. -- John
On 23 Nov 2004, at 15:51, John Goerzen wrote:
On Tue, Nov 23, 2004 at 04:30:21PM +0000, Keean Schupke wrote:
I am sure this discussion has happened before, but I think for pure functions, returning Either Error Result is the way to go.
That's certainly possible, but extremely tedious.
It sounds to me like the Either approach is what you are asking for, though? You want: catchJust :: (Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a Can't you write: catch :: (c -> Either Error a) -> c -> (Error -> a) -> a catch code value handler = case code value of Right res -> res | Left e -> handler e ..or, to use the either idiom catch code value handler = either (\res -> res) (\e -> handler e) (code value) (the extension to catchJust is left to the reader) The point is : the Either approach is the correct way to represent the notion of 'pure functional exceptions', you don't need language support: and you can build something that looks like language support as combinators? Jules
On 2004 November 23 Tuesday 10:51, John Goerzen wrote:
for pure functions, returning Either Error Result is the way to go.
One example: I've written an FTP client library. For every operation, there are several possible outcomes... mainly: success, low-level network error, or server error.
Having to pick apart an Either from every command to CD, set transfer types, etc. would get so tedious that the code would, I think, become spaghetti fast.
The way to deal with those kinds of details is to use Either in a monad. I'm skeptical of the need for dynamic scope in conventional exception handling, so I took a shot at this problem, with satisfying results. (I'm not keen on dynamic typing for that matter, but don't know of a nice way to avoid it.) Excerpts are below. The full sample code is at http://pkturner.org/exception.tar The Haskell Main module using the FTP library is comparable to your Python example. main = runException $ do ftp("ftp.kernel.org") ( cwd "pub/linux/kernel/v2.4" `except` (\(ErrorPerm e) -> lift $ do putStrLn ("caught temp error in cwd: " ++ e) exitWith (ExitFailure 2)) ) retrbinary "RETR ChangeLog-2.4.13" (\block -> write block) quit `except` (\(ErrorPerm e) -> lift $ do putStrLn ("Permissions error " ++ e) exitWith (ExitFailure 2)) `except` (\(ErrorTemp e) -> lift $ do putStrLn ("Temporary error, please try again later" ++ e) exitWith (ExitFailure 1)) `except` (\(ErrorFTP e) -> lift $ do putStrLn ("Other FTP error" ++ e) exitWith (ExitFailure 2)) `except` (\(ErrorAll e) -> lift $ do putStrLn ("Non-FTP error" ++ e) exitWith (ExitFailure 3)) Also, the FTP module demonstrates that it's straightforward to add new classes of errors in the exception hierarchy. data ErrorPerm = ErrorPerm String deriving (Typeable) instance Hierarchical ErrorPerm where parent (ErrorPerm msg) = Parent $ ErrorFTP msg data ErrorTemp = ErrorTemp String deriving (Typeable) instance Hierarchical ErrorTemp where parent (ErrorTemp msg) = Parent $ ErrorFTP msg data ErrorFTP = ErrorFTP String deriving (Typeable) instance Hierarchical ErrorFTP where parent (ErrorFTP msg) = Parent $ ErrorAll msg type FTP = Exception IO ftp :: String -> FTP () ftp str = if take 3 str == "ftp" then lift (return ()) else raise $ ErrorPerm ("ftp " ++ str)
On 24 Nov 2004, at 16:21, Scott Turner wrote:
On 2004 November 23 Tuesday 10:51, John Goerzen wrote: The way to deal with those kinds of details is to use Either in a monad. I'm skeptical of the need for dynamic scope in conventional exception handling, so I took a shot at this problem, with satisfying results. (I'm not keen on dynamic typing for that matter, but don't know of a nice way to avoid it.) Excerpts are below. The full sample code is at http://pkturner.org/exception.tar
Ok, I glanced through your code, and you seem to be reimplementing many of the ideas in the MonadError class, which also makes Either into a Monad. http://www.haskell.org/ghc/docs/latest/html/libraries/base/ Control.Monad.Error.html Would you care to clarify the important differences? It looks like your support for hierarchies of exceptions is an enhancement? The external interface looks nice, the internal implementation with fromDynamic doesn't seem pretty though :-( Jules
On Wed, Nov 24, 2004 at 06:12:27PM +0000, Jules Bean wrote:
Ok, I glanced through your code, and you seem to be reimplementing many of the ideas in the MonadError class, which also makes Either into a Monad.
http://www.haskell.org/ghc/docs/latest/html/libraries/base/ Control.Monad.Error.html
That documentation is really poor. I really have no idea what problem this module solves, why I should use it, or how I should use it from the documentation presented. It would be great if someone clarified that a bit. I note, though, that "making an Either into a Monad" doesn't do anything to deal with asynchronous exceptions. -- John
On 24 Nov 2004, at 18:28, John Goerzen wrote:
I note, though, that "making an Either into a Monad" doesn't do anything to deal with asynchronous exceptions.
We may be talking at cross purposes here. If, by 'asynchronous exceptions' you mean that exceptions may lurk arbitrarily deeply within user defined data structures then using Either *does* solve this problem. Using 'Either' makes it crystal clear exactly where exception values can exist. Rather than being arbitrarily deep in the structure, exceptions can only exist at the nodes that you choose to decorate with Either: which, in the obvious usage pattern, is only the root node. This doesn't mean that exceptions can't be *raised* at any depth, it just means that deep exceptions are propagated. In other words, the 'Either' pattern will actually, as a side effect, force complete evaluation when you check for exceptions. [To be clear: it will only do this if you choose to use the usage pattern where only the root node is decorated as 'Either' and your functions propagate the Either-ness] Writing such code 'by hand' would involve a large amount of wrapping and unwrapping (as you observed). That's all "making Either into a Monad" will cure: it makes the wrapping and unwrapping of Lefts and Rights a product of haskell's clever monadic syntax. It also makes you choose an evaluation order, which removes non-determinism (the first exception is the propagated one). If that isn't what you meant by asynchronous exceptions then we are indeed talking at cross-purposes! Jules
On Wed, Nov 24, 2004 at 07:14:28PM +0000, Jules Bean wrote:
On 24 Nov 2004, at 18:28, John Goerzen wrote:
I note, though, that "making an Either into a Monad" doesn't do anything to deal with asynchronous exceptions.
[ snip]
If that isn't what you meant by asynchronous exceptions then we are indeed talking at cross-purposes!
Thanks for the detailed explanation, but indeed we are :-) I was referring to exceptions generated by things such as signals, interrupts, certain network errors, stack problems, etc. Exceptions are are not necessarily generated as a direct result of a particular piece of Haskell code. Simon called them asynchronous in his paper, so I'm just stealing the term :-) -- John
John Goerzen wrote:
I note, though, that "making an Either into a Monad" doesn't do anything to deal with asynchronous exceptions. [ snip] I was referring to exceptions generated by things such as signals, interrupts, certain network errors, stack problems, etc.
How would you like asynchronous exceptions to fit in? Your original request mentioned that it was annoying to have to use the IO monad for all exceptions, particularly when the exceptions occur in deterministic code. But the IO monad is plainly appropriate for asynchronous exceptions. If what you need is to catch asynchronous exceptions as soon as possible and propogate them as Either-based exceptions, then my sample code can be readily extended in that way. For example, you could catch a network error and propogate it with a message saying that the error occurred during an FTP write operation. This is possible because the monad that's used for the FTP module combines Either with IO.
On Thursday 25 November 2004 00:29, Scott Turner wrote:
John Goerzen wrote:
I note, though, that "making an Either into a Monad" doesn't do anything to deal with asynchronous exceptions.
[ snip]
I was referring to exceptions generated by things such as signals, interrupts, certain network errors, stack problems, etc.
How would you like asynchronous exceptions to fit in? Your original request mentioned that it was annoying to have to use the IO monad for all exceptions, particularly when the exceptions occur in deterministic code. But the IO monad is plainly appropriate for asynchronous exceptions.
I think the explanation was a bit misleading. Asynchronous exceptions are exceptions that can occur *anywhere*, even in purely functional code and even if the code itself is completely free of bottoms. A good example is 'heap overflow', i.e. insufficient memory even after the GC was run. Another example are interrupts and unix signals: they can occur at any time, regardless of what the program is doing. More generally, whenever you can send a message to a thread that isn't expecting one. Control.Exception defines 'throwTo' that gets a threadId as argument. This causes the target thread to be interrupted with an asynchronous exception. Asynchronous exceptions are a rather recent addition to ghc. How they relates to your (very interesting) EitherMonad solution is not completely clear to me, either. Ben
Gosh, I shouldn't post to mailing lists after midnight. Please excuse my needless explanations. I didn't understand your answer at first. Cheers, Ben
On 2004 November 24 Wednesday 13:12, Jules Bean wrote:
On 24 Nov 2004, at 16:21, Scott Turner wrote:
On 2004 November 23 Tuesday 10:51, John Goerzen wrote: The way to deal with those kinds of details is to use Either in a monad.
Ok, I glanced through your code, and you seem to be reimplementing many of the ideas in the MonadError class, which also makes Either into a Monad.
Yes, it's a case of reinventing the wheel, the addition being as you point out, the support for a hierarchy of exceptions. My code could be simplified by building on MonadError rather than doing the monad from scratch.
Would you care to clarify the important differences? It looks like your support for hierarchies of exceptions is an enhancement?
The hierarchy of types that can catch the exception is determined at the point where the exception is raised. So the error value is represented in the monad using a list of Dynamic values, lazily. In the past I have avoided 'Dynamic', but it's needed here if a perfect match between the catch and the raising of the exception is to be detected based on type -- this is what programmers have come to expect. Each error type is an instance of Hierarchical, so that its errors may be considered part of a larger category of errors. In the instance definition, 'parent' specifies how the error appears if it is caught by a handler expecting then next more general error type.
On 24 Nov 2004, at 19:16, Scott Turner wrote:
Each error type is an instance of Hierarchical, so that its errors may be considered part of a larger category of errors. In the instance definition, 'parent' specifies how the error appears if it is caught by a handler expecting then next more general error type.
Dynamic seems to be a sledgehammer for this particular nut. Can we not have a type class for 'subtyping' (like your hierarchical). Can multi-parameter typeclasses do this? We want: class SubType a b where inject :: a -> b and instance SubType a b,SubType b c => SubType a c where inject :: a -> c inject some_a = inject ((inject some_a) :: b) and then the definition of `except` requires that the SubType relates the type of the handler and the type of the actual error? Jules
Last week John Goerzen asked about exceptions in Haskell. I responded with some code that supports a hierarchy of exception types. Jules Bean reacted that "the internal implementation with fromDynamic doesn't seem pretty though". Although dynamic types reflect the common implementation of exceptions in Java and C++, I wondered to what extent this coding style could be supported with static type checking. I've come up with a framework that does so. The Haskell Main module using the FTP library remains comparable to John Goerzen's Python example. main = runException $ runException $ do ftp("ftp.kernel.org") ( cwd "/pub/linux/kernel/v2.4" `catchException` (\(ErrorPerm e) -> liftIO $ do putStrLn ("caught temp error in cwd: " ++ e) exitWith (ExitFailure 2)) ) retrbinary "RETR ChangeLog-2.4.13" (\block -> write block) quit `catchException` (\(ErrorPerm e) -> liftIO $ do putStrLn ("Permissions error " ++ e) exitWith (ExitFailure 2)) `catchException` (\(ErrorTemp e) -> liftIO $ do putStrLn ("Temporary error, please try again later " ++ e) exitWith (ExitFailure 1)) `catchException` (\(FTPError e) -> liftIO $ do putStrLn ("Other FTP error " ++ e) exitWith (ExitFailure 2)) `catchException` (\(AnyError e) -> liftIO $ do putStrLn ("Non-FTP error " ++ e) exitWith (ExitFailure 3)) The FTPError module demonstrates how to add new classes of errors in the exception hierarchy. In the statically typed framework this is more verbose than previously. The full sample code is at http://www.pkturner.org/exception2.tar 1. The revised implementation builds on ErrorT rather than rolling its own. 2. The Exception class associates an error type with a monad that can throw and catch the type. 3. A monad like FTPException may propagate several error types. These are held as alternatives in a master type FTPError. A consequence is that exception handler functions return a Maybe result, so that a handler for a subtype can be promoted to a handler for its supertype. This is the first method for building an exception hierarchy, enhancing the basic use of ErrorT (from Control.Monad.Error). 4. FTPException is also a Subexception, meaning that it expands on the set of error types supported by an inner monad. This is the second method for building an exception hierarchy. It goes beyond lifting the actions of the inner monad in a couple of respects. a. The FTPException monad's actions are invoked from a more basic monad, which becomes its inner monad. If any error is thrown and not caught, the FTPException code will return to the invoking monad, propagating the error using a type which is natively understood by that monad. b. If one of the inner monad's errors is thrown, it can be caught and handled using the full abilities of the FTPException monad. 5. The root exception monad, AnyException, supports one error type, AnyError.
John Goerzen wrote:
myfunc :: String -> Int
This does some sort of string parsing and returns an Int. Or it may raise an exception if it couldn't parse the string. But it would do that every time.
Now, let's say we have a non-IO catchJust. Of course, if we never need the value, we never run the function -- or catchJust. If we do need the value, we run the function in the context of catchJust. If it raises our exception, catchJust handles it and returns some default. If it raises no exception, it's the same as having no handler at all. And if it raises some other exception, it's also the same as having no handler at all.
So what am I missing here?
myfunc might raise more than one exception. For example, myfunc = error "x" + error "y" Haskell doesn't specify which exception will actually be thrown, and this indeterminism is modeled by putting exception-catching functions in the IO monad. There's a paper explaining it [1]. I'm not convinced this is the best solution. I can't think of a situation in which there's a piece of code that throws different exceptions depending on evaluation order, /and/ I care which one of those I catch. If each particular implementation were just consistent about which exception it reports in these situations, I think the exception-catching functions would be pure. In any case, mapException is pure, and it's good enough for most of the cases where one might want to catch exceptions outside the IO monad. The second problem with exceptions in Haskell, which I think is rather more serious, is that they can hide inside of data structures. This means that you can't, for example, wrap an exception handler around (readConfigFile :: FilePath -> IO [ConfigOption]) and be sure that you will catch any exceptions that are thrown during the parsing of the configuration file. Depending on how readConfigFile is written, those exceptions may be hiding inside the returned list, where they won't be noticed until some other part of the program inspects that part of the list, at which point your exception handler is no longer in scope. The only remedy is to use deepSeq (which really ought to be derivable). A question I don't know the answer to is whether (catchJust :: a -> Maybe a), which lumps all exceptions into a Nothing return, is pure. It seems like it would be; if so, why don't we have it? Or do we? -- Ben [1] http://research.microsoft.com/~simonpj/Papers/imprecise-exn.htm
On Tue, Nov 23, 2004 at 05:20:19PM +0000, Ben Rudiak-Gould wrote:
So what am I missing here?
myfunc might raise more than one exception. For example,
myfunc = error "x" + error "y"
Gotcha. That's the piece I was missing! [ snip ]
those I catch. If each particular implementation were just consistent about which exception it reports in these situations, I think the exception-catching functions would be pure. In any case, mapException is pure, and it's good enough for most of the cases where one might want to catch exceptions outside the IO monad.
Well, I'm maving trouble wrapping my head around how I could use it in a pure enviroment. It's defined as: mapException :: (Exception -> Exception) -> a -> a I *think*, from reading the source, that it is returning the value 'a' if there is no exception, or the mapped exception (or set of them?) if there is. That doesn't really help me, though, because I still have an exception that I must catch in the IO monad.
The second problem with exceptions in Haskell, which I think is rather more serious, is that they can hide inside of data structures. This
I've thought about some, and really, that doesn't bother me. In practice, if I never access the part of the structure with the exception, I probably never care that the parsing failed. In a well-written imperative program, I would have never called the parser in this situation.
means that you can't, for example, wrap an exception handler around (readConfigFile :: FilePath -> IO [ConfigOption]) and be sure that you
Is someone else working on configuration file parsing, or are you just remembering my earlier question? :-)
will catch any exceptions that are thrown during the parsing of the configuration file. Depending on how readConfigFile is written, those exceptions may be hiding inside the returned list, where they won't be noticed until some other part of the program inspects that part of the list, at which point your exception handler is no longer in scope. The
I lost you here. Isn't the exception handler just another function? That is, closures would handle this just like anything else?
A question I don't know the answer to is whether (catchJust :: a -> Maybe a), which lumps all exceptions into a Nothing return, is pure. It seems like it would be; if so, why don't we have it? Or do we?
Are you proposing a hypothetical here? (GHC's catchJust returns an IO a). -- John
John Goerzen wrote:
On Tue, Nov 23, 2004 at 05:20:19PM +0000, Ben Rudiak-Gould wrote:
In any case, mapException is pure, and it's good enough for most of the cases where one might want to catch exceptions outside the IO monad.
Well, I'm maving trouble wrapping my head around how I could use it in a pure enviroment. It's defined as:
mapException :: (Exception -> Exception) -> a -> a
I *think*, from reading the source, that it is returning the value 'a' if there is no exception, or the mapped exception (or set of them?) if there is.
Basically, in the paper, exceptions are modeled as extra values which inhabit every type. Bool, for example, contains the following values: True, False, _|_, and all nonempty /sets/ of values of type Exception. (The paper actually says that _|_ is the set of all exceptions, but I think that's an error. _|_ is quantitatively different from any set of exceptions.) My expression (error "x" + error "y") has the value { Control.Exception.ErrorCall "x", Control.Exception.ErrorCall "y" }. The exception-catching functions choose a single exception from the set by some unknown means; since they're in the IO monad it needn't be predictable which one they choose. If its second argument is a set of exceptions, mapException passes each exception through the supplied function to get a new set, and returns that. Otherwise it returns its second argument unchanged.
That doesn't really help me, though, because I still have an exception that I must catch in the IO monad.
It's useful for adding information to an exception as it propagates, or translating exceptions at abstraction boundaries. (Except it doesn't quite work, because the exceptions can hide from the handler -- see below.)
will catch any exceptions that are thrown during the parsing of the configuration file. Depending on how readConfigFile is written, those exceptions may be hiding inside the returned list, where they won't be noticed until some other part of the program inspects that part of the list, at which point your exception handler is no longer in scope. The
I lost you here. Isn't the exception handler just another function? That is, closures would handle this just like anything else?
I didn't phrase it well. Denotationally the problem is that, e.g., catch (return [1,2,undefined]) (\e -> return [4,5,6]) === return [1,2,undefined] whereas often (usually!) I'd prefer it be equivalent to (return [4,5,6]). Every exception-handling function, including mapException, behaves this way: myMapException = mapException (\e -> error "internal error") myMapException (throwDyn MyError) === error "internal error" myMapException [throwDyn MyError] === [throwDyn MyError] If the first element of the list returned by that last example is ever demanded, MyError will be thrown, and it will /not/ be passed through (\e -> error "internal error"). This means that I can't (straightforwardly) wrap an exception handler around my function to catch exceptions that I don't want the user, or another part of the program, to see. Of course, this isn't a bug and can't exactly be "fixed". It's just an interaction between the traditional throw/catch exception model and non-strictness that I consider to be unfortunate. Most of the cases where I might otherwise have used exceptions in Haskell have fallen afoul of this problem.
A question I don't know the answer to is whether (catchJust :: a -> Maybe a), which lumps all exceptions into a Nothing return, is pure.
Are you proposing a hypothetical here? (GHC's catchJust returns an IO a).
Sorry, that was a braino. It's a totally different function. I should have called it something like exceptionToMaybe. The intended semantics is / Nothing if x is a set of exceptions exceptionToMaybe x = | _|_ if x is _|_ \ Just x otherwise -- Ben
Ben Rudiak-Gould <Benjamin.Rudiak-Gould@cl.cam.ac.uk> writes:
The intended semantics is
/ Nothing if x is a set of exceptions exceptionToMaybe x = | _|_ if x is _|_ \ Just x otherwise
What is exceptionToMaybe (f 0 + error "x") where f x = f x ? -- __("< Marcin Kowalczyk \__/ qrczak@knm.org.pl ^^ http://qrnik.knm.org.pl/~qrczak/
I, too, had a gripe about this, and was pointed to an excellent paper that explains all: A Semantics for Imprecise Exceptions (1999) Simon Peyton Jones, Alastair Reid, Tony Hoare, Simon Marlow, Fergus Henderson SIGPLAN Conference on Programming Language Design and Implementation http://citeseer.ist.psu.edu/peytonjones99semantics.html #g -- At 16:12 23/11/04 +0100, Johannes Waldmann wrote:
The other annoying thing is forcing it to run in the IO monad.
necessarily so, since Haskell has non-strict semantics so it's not so clear when an exception is actually raised (you might have left the block that textually contained the offending expression , and the exception handler, a long time ago) -- -- Johannes Waldmann, Tel/Fax: (0341) 3076 6479 / 6480 -- ------ http://www.imn.htwk-leipzig.de/~waldmann/ ---------
_______________________________________________ Haskell mailing list Haskell@haskell.org http://www.haskell.org/mailman/listinfo/haskell
------------ Graham Klyne For email: http://www.ninebynine.org/#Contact
On Tue, 23 Nov 2004, John Goerzen wrote: (snip)
I've been using Haskell for 1-2 months now, and feel fairly comfortable (snip) catchJust :: (Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a (snip)
Yes, this was one of the first things that bothered me, too, when I started actually writing much Haskell code: I wanted a non-monadic version of try/catch. Because a pure function should always return the same value given the same arguments, the behaviour of such a try/catch must be made quite deterministic: for example, perhaps it should only return exceptions generated in its own execution thread (to make it entirely synchronous), and perhaps all the exceptions that could be generated in its evaluation should be generated (so we don't learn anything about evaluation order - after the first exception, we may need to carry on evaluating other parts of the expression to find what other exceptions there are). I seem to have learned to live with the lack of such non-monadic exceptions: often I use monads for this sort of error propagation thing, but not the standard try/catch in the IO monad because I avoid the IO monad wherever possible because functions in the IO monad are so unconstrained by the type system with regard to what effects they could have. -- Mark
John Goerzen wrote:
So why do we have:
catchJust :: (Exception -> Maybe b) -> IO a -> (b -> IO a) -> IO a
instead of:
catchJust :: (Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a
As I'm sure you have gathered from all the answers you can't have the latter and keep Haskell pure. But there is an interesting alternative (at least theoretically). You could have a function like mkCatchJust :: IO ((Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a) And you would use it by do cj <- mkCatchJust .... cj .... The cj function can be used in non-IO code and will behave just as you want. But you have to create it in the IO monad, since it's behaviour is not deterministic (especially not in the face of async exceptions). The tricky thing is to implement it, because cj should really only be used once. Why? Because it has to behave the same way for the same arguments every time. This is not easily implemented. :( -- Lennart
On Thu, Nov 25, 2004 at 07:52:43PM +0100, Lennart Augustsson wrote:
As I'm sure you have gathered from all the answers you can't have the latter and keep Haskell pure. But there is an interesting alternative (at least theoretically). You could have a function like
mkCatchJust :: IO ((Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a)
How is that different from this? mkReadFile :: IO (FilePath -> String) This is wrong. Even if I get a function as a result of an IO computation, I expect that function to be pure.
The cj function can be used in non-IO code and will behave just as you want. But you have to create it in the IO monad, since it's behaviour is not deterministic (especially not in the face of async exceptions).
IO monad shouldn't be used to create non-deterministic functions. The whole point in using IO monad is that functions stay pure, regardless of their origin. Best regards, Tomasz
On 25 Nov 2004, at 19:24, Tomasz Zielonka wrote:
On Thu, Nov 25, 2004 at 07:52:43PM +0100, Lennart Augustsson wrote:
As I'm sure you have gathered from all the answers you can't have the latter and keep Haskell pure. But there is an interesting alternative (at least theoretically). You could have a function like
mkCatchJust :: IO ((Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a)
How is that different from this?
mkReadFile :: IO (FilePath -> String)
This is wrong. Even if I get a function as a result of an IO computation, I expect that function to be pure.
Well, you can presumably give it a semantics which is pure. Whether it makes good sense is another question. The semantics is 'it creates a function which, when invoked, will return the contents of the file at some fixed (undefined) time'. By the same token, you can just stick the function strangeReadFile :: FilePath -> String into the language. As long as it is memoized, always returning the same value, it doesn't break beta-reduction. I call it 'strange' because the time that the file is actually read is not guaranteed, so if you read more than one file in your program, you have no guarantee that you are reading a constant total state that actually existed at any point in time. (Before you think this sounds unbearably horrible, there is at least one commercially sold RDBMS which has this semantics on its select statements ;P) Jules
Jules Bean wrote:
By the same token, you can just stick the function strangeReadFile :: FilePath -> String into the language. As long as it is memoized, always returning the same value, it doesn't break beta-reduction. I call it 'strange' because the time that the file is actually read is not guaranteed, so if you read more than one file in your program, you have no guarantee that you are reading a constant total state that actually existed at any point in time. (Before you think this sounds unbearably horrible, there is at least one commercially sold RDBMS which has this semantics on its select statements ;P)
It is also strange by the fact that strangeReadFile can very well be different functions at different runs of the program. That's why I think such a function should be generated by the IO monad. (If you want it at all!) -- Lennart
Tomasz Zielonka wrote:
On Thu, Nov 25, 2004 at 07:52:43PM +0100, Lennart Augustsson wrote:
As I'm sure you have gathered from all the answers you can't have the latter and keep Haskell pure. But there is an interesting alternative (at least theoretically). You could have a function like
mkCatchJust :: IO ((Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a)
How is that different from this?
mkReadFile :: IO (FilePath -> String)
This is wrong.
Why is this wrong?
Even if I get a function as a result of an IO computation, I expect that function to be pure.
Yes, so do I. The function returned will have to be pure. Pure in the sense that given the same argument it always has to return the same result. In the case of mkReadFile this is not too hard to implement. I admit that it is rather awkward, but it can be pure. -- Lennart
Tomasz Zielonka wrote:
On Thu, Nov 25, 2004 at 07:52:43PM +0100, Lennart Augustsson wrote:
As I'm sure you have gathered from all the answers you can't have the latter and keep Haskell pure. But there is an interesting alternative (at least theoretically). You could have a function like
mkCatchJust :: IO ((Exception -> Maybe b) -> (c -> a) -> c -> (b -> a) -> a)
How is that different from this?
mkReadFile :: IO (FilePath -> String)
This is wrong. Even if I get a function as a result of an IO computation, I expect that function to be pure.
BTW, I couldn't stop myself. Here's (a simple version of) mkReadFile: mkReadFile :: IO (FilePath -> String) mkReadFile = do names <- traverse "/" files <- mapM readFile names let find name = case lookup name (zip names files) of Just file -> file Nothing -> error "file not found" return find traverse :: FilePath -> IO [FilePath] traverse d = do fs <- getDirectoryContents d let fs' = map ((d ++ "/") ++) (fs \\ [".", ".."]) fss <- mapM traverse fs' return (concat fss) `catch` \ e -> return [d] I don't really recommend it being used. :) -- Lennart
participants (12)
-
Ben Rudiak-Gould -
Benjamin Franksen -
Graham Klyne -
Johannes Waldmann -
John Goerzen -
Jules Bean -
Keean Schupke -
Lennart Augustsson -
Marcin 'Qrczak' Kowalczyk -
Mark Carroll -
Scott Turner -
Tomasz Zielonka