On Sun, 2007-10-21 at 23:18 -0700, Erick Tryzelaar wrote: > I've been spending the past couple weeks thinking about errors. This > is one thing we haven't really explored much.
There are some hard choices. > Currently, all that we > really do is just error out, but that's not really a great solution. Actually I think it is :) If a program encounters an error, the semantics are no longer guaranteed: all bets are off. Terminating is desirable. One hopes it is possible the system isn't that corrupt that the attempt at termination itself fails. If the program can 'handle' the error .. it isn't an error. The issue here is: consider a web server. An error in a plugin should probably terminate the plugin but not the server. This means the concept of 'error' is scoped (localised, etc). An error at one level of abstraction may need to be translated as it crosses an abstraction boundary. In particular you might catch an exception and convert it to a variant (i.e. return an error code). Obviously with low level C calls, the reverse happens too: an error code is translated to an exception. The key issue here is how to properly implement delimited continuations. At present Felix can 'sort of' do this with a non-local goto. Note executing one of these doesn't work if the machine stack is in the way. However there is a 'throw' construction which DOES work, provided there is only ONE stack in the way. In that case a continuation is thrown as a C++ exception which unwinds the machine stack, and is then caught at the top level, where it is invoked to execute the non-local goto, which unwinds the spaghetti stack. If this hits a function local procedure calls, it is simply lost, because the compiler generates a tight loop: while(p)p->resume(); and the unwinding code can't cope with running off the spaghetti stack and going back to the machine stack. The problem is non-local goto simply unwinds the stack until it hits a procedure whose con_t* address equals that of the closure containing the non-local goto... this might not exist on the current spahetti stack, indeed it might not exist at all (the frame will always exist because it is reachable, but it may not be 'live', so if it is re-entered when it comes to return all hell breaks lose. With any luck the caller address has been NULL'ed out, but in this case the continuation probably should have been resumed in the first place). Bottom line is that we lack both a proper theoretical model for delimited exceptions, as well as a viable implementation :) > So, while I don't really have a solution yet, I figure we should > probably start a dialog on how we should do things. But the first step is probably get some actual stream I/O working in a simple way people can use it. That may help get some users. Even I have no idea how to read and write disk files in Felix (and I wrote a lot of the I/O abstraction code.. RF did most of the low level I/O). My take on this kind of 'discussion' is that you need USE CASES to make progress. > So, some > definitions. I found a nice list here: > > http://groups.google.com/group/fa.haskell/msg/77dab6e52691fa9c > > 1. Expected error: The result can't be computed from this data; the > caller was not supposed to check this beforehand because it would > duplicate work of the function, or there would be a race condition, or > the design is simpler that way. This isn't an error. The correct result is a variant (union) type. For example union det = Singular | Det of double; fun determinant(x:matrix): det => ... ; > 2. Program error: The given function was not supposed to be invoked in > this way by other parts of the program (with these arguments, or with > this external state, in this order wrt. other functions etc.). Yep. In this case termination is acceptable, however sometimes you might want to continue, in case the error is harmless (this is generally called a 'warning' :) > 3. Out of resource: The function is sorry that it was not able to > produce the result due to limited memory or arithmetic precision or > similar resources. "Not yet supported" is also in this group. That's not an error: as above, the variants can handle this. > 4. Impossible error: > So with this, I think we can structure a bit on how we can design how > deal with this problems. First, lets critique a couple of the > handlers: > > 1. Abort: Not really a practical solution. Abort is really only > appropriate for unrecoverable errors. This may or may not be > recoverable, so we cannot make the decision for the end user. I don't agree. In the case of an actual error, abort is the only viable solution. If you write a simplistic piece of code, which is typical of many scripts, this is exactly what you want, and you want it guaranteed. By using an API that doesn't support error handling, you've simplified your code, and it is likely it will run. If it doesn't, you want to know, LOUDLY :) > 2. Signals: Really only used for the kernel to talk to us, and it's > generally a pain to deal with. However we have no choice for some events, eg Ctrl-C. > 3. Return Values: Using unions we can force people to deal with > errors. Too much error handling can get messy though, but things like > monadic combinators and sugar > can help. Yes. In particular, some 'automatic' error propagation, such as FP NaN, can be translated into a delimited exception. Roughly, where the NaN stops being silent and starts being signalling, then gets used, whatever trap is put there can be jumped to immediately inside the monad, if the monad is such that there is no way out of returning a NaN. This can probably be determined by the type system, strictness analysis etc etc. > 4. Unchecked exceptions: As opposed to return codes that you have to > deal with, exceptions you might not even know that it might occur. You > also lose the locality of the error, and it might be too late to > recover. Felix usually throws C++ exceptions, except where I have been too lazy and did an abort. However they cannot be caught, except in the driver, which catches all the standard ones. > 5. Checked exceptions: Now you know what a function can through, but > things get really messy with evolving code. An throw specification is just a negative constraint on the domain of the function. you might write: fun f(x:int where throw divide_by_zero)=> 1/(x+1); which means fun f(x:int where x != 1)=> .. stated negatively, however the condition isn't explicit. > Finally, I'd like to try to exploit some of the interesting features > of felix, such as our unions, fibers, and threads. These provide some > nice primitives that we could build from. > > One of the early suggestions was that we provide two interfaces for > fail-able functions: one that returns an opt value, and one that > aborts if the function fails. Another was to use a fiber and pass in > an "error channel" to a function, and if an error occurred, write the > error into the channel. What if we were able to combine all of these? Actually, you could invert that question, and make it a REQUIREMENT. > I've always imagined felix in the more > traditional-small-number-of-threads view, but I've been reading a book > on erlang, and have been very inspired by it. I love the idea of > having tons of independent processes working and managing the health > system. One interesting aspect to erlang is how they do error > handling. While they still do unchecked exceptions for internal > errors, if an error isn't handled, the process is killed and then an > "exit" signal is sent to all the linked processes. These processes can > either catch the exit signal and handle it, or let themselves be > killed by the signal. Here's some of the docs: > > http://erlang.org/course/error_handling.html Interesting. However note Erlang is highly dynamic AND it uses a purely functional model of many processes and message passing, whereas Felix is oriented towards threads and shared memory (at the moment). It would be really cool if we could *merge* Felix and Erlang, as they seem to be complimentary. > What if we were able to do something like this? While we might be able > to do this with runtime modification, it might also be able to do this > with monads. I doubt it can be done without runtime support. The primitives Felix has are mixed: channels/fthreads are cool, but they're restricted to a single CPU. pchannels/pthread are similar, and will work on a multi-processor, and even look like fthread, but the semantics are quite different: pthreads can deadlock, fthreads can't. Processes with message passing are simplest since they don't need direct locking, like fthreads, however with no shared memory, communication exchanges are required for synchronisation which amount to locks anyway. Then, Felix has the non-local goto (as well as the usual procedure call/spaghetti stack stuff) and some hacks with exceptions. This stuff is NOT right as explained above. The real problem is that the machine stack is 'supposed' to be just an optimisation of procedural code, and functions are really procedures. However which ever C++ code we make to handle this stuff remains limited by C++ itself. I sometimes wish I could just write assembler, it would get rid of all the problems of using a target without the proper semantics. > Consider monadic exceptions. This means that writing > something like this: > > try > val x = do_something (); > do_something_else x; > with > | ?e => handle_error e > endtry; > > Could just be a series of function calls like this: > > match do_something () with > | Failure ?e => handle_error e > | Ok ?x => > match do_something_else x with > | Failure ?e => handle_error e > | Ok () => () > endmatch > endmatch; Yes, such sugar is quite possible, but it won't do what you might expect from Ocaml or C++ because your desugared form shows checking a variant .. which means we have to know what variant to check. Don't forget, Felix doesn't have type inference, so stuff like | Failure ?e => .. will not work 'in general', we need to know the actually type variables used to popular the union: union Return[t,e] = | Ok of t | Failure of e > We could use this to simulate what erlang does. Say we spawn off a > function that calls some possibly exceptional code. If the code isn't > handled in a monad, then the thread would error out, and the exception > would be written across the channel to the linked processes. Then, if > *those* processes don't handle the exit, it would be passed down the > line. Have a look at the example Monad in Felix, which propagates error condition. It seems nice in theory .. but look at the code. It isn't that simple to use! The Haskell 'do' notation may help, though I don't really understand it. > Working this all out is try, though. Erlang gets away with a lot > because it's a dynamic language. We'd have to figure out some way of > wrapping the values unobtrusively since the type expressions could get > pretty ugly: > > schannel[failure[process_t, failure[error_t, ok_t]]] > > if you wanted to represent a process that can write exceptions down a > channel, but then itself can error out. I'm not sure how to solve this > other than through type inference. Yes, that's really a major part of the problem. Now -- Felix is (surprise!) actually intended to be dynamically typed, like all good scripting languages! The current static typing is there because trying to add optional static typing to a language after designing a dynamic type system is a disaster. What we really need is 'bounded' or 'delimited' dynamics. Note for example how Haskell does typeclasses across translation unit boundaries, by passing dictionaries. Or how Ocaml does classes by run time Hashtbl based method dispatch. Both these things are significantly dynamic .. yet are also statically typed. Of course a 'match' is the same: there really is a runtime dispatch on the variant tag. -- John Skaller <skaller at users dot sf dot net> Felix, successor to C++: http://felix.sf.net ------------------------------------------------------------------------- This SF.net email is sponsored by: Splunk Inc. Still grepping through log files to find problems? Stop. Now Search log events and configuration files using AJAX and a browser. Download your FREE copy of Splunk now >> http://get.splunk.com/ _______________________________________________ Felix-language mailing list Felix-language@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/felix-language