One of the most remarkable and compelling features of Common Lisp is, of course, its exception handling system.
Moreover, in my opinion, personally, the opinion that such an approach to exceptions is the only correct one for all imperative languages, and for this simple reason:
The mechanism of "exceptions" (or, as they are called in the world of CL - conditions) in Common Lisp is separated from the mechanism of stack promotion, and this, respectively, allows you to handle any pop-up in the program exceptional (and not only exceptional) situations right in that place, where they originated, without losing the program execution context, which entails ease of development, debugging, and in general, ease of constructing the logic of the program.
')
Perhaps it should be said that the Common Lisp Condition System, despite its uniqueness in the environment of high-level programming languages, is very close to many low-level tools of modern operating systems known to many developers, namely, UNIX synchronous signals and, much closer, the mechanism of SEH (Structured Exception Handling) from windows. Leading CL implementations base such flow control elements as an exception handling mechanism and stack promotion on them.
Despite the absence of a similar mechanism in many other (if not all) imperative programming languages, it can be implemented in a more or less sane form on most of them. In this article, I will describe the implementation in C #, sorting out the very concept of this approach to “exceptions”.
To fully implement CLCS from a programming language, or rather even from its runtime, several things are required:
- Strict stack model execution. Here I mean the absence of full-fledged "continuations" in the language. This point is rather conditional, but since the continuations introduce a huge blur into the mechanism for controlling the flow of computations, and do not allow determining with sufficient accuracy the main primitives that the CLCS repels, their presence is highly undesirable.
- Higher order functions, anonymous functions, and closures. Of course, if you try, you can implement everything through objects and classes, but in this case it will be extremely inconvenient to use all of this, in my opinion.
- Dynamic environments and, in particular, dynamic variables. About dynamic environments and variables I wrote in more or less detail in my article on the semantics of modern Lisp: love5an.livejournal.com/371169.html
In the absence of a similar concept in a programming language, it is, however, emulated using the following two points: - Operators try, catch and throw, or their analogs. These statements are in any programming language that supports exceptions.
- Primitive UNWIND-PROTECT or its equivalent (block try-finally, RAII, etc.).
We will transfer the following CL exception handling primitives to C #:
- handler-bind - sets an exception handler for the duration of the operator’s body. When catching an exception, the handler may decide to roll out the stack, but is not required to do so.
- handler-case - sets an exception handler for the execution time of the operator body. When catching an exception, the stack is unwound and the operator returns the value calculated in the body of the handler.
- signal - signals the occurrence of an exception to the higher-level handler, if present.
- error - signals the occurrence of an exception to a higher-level handler, and in the absence of this, or in the case of failure of all handlers to cope with the exception, throws an exception using the usual method, i.e. operator throw (This is in our implementation. In Common Lisp, the error function calls the debugger if it is connected, or, otherwise, it terminates a separate computational flow (thread) or the entire lisp system.)
- restart-bind - installs a "restart", not causing the stack promotion mechanism. Restarting is a function in the current dynamic environment (see the link to the article above) that can somehow respond to the exception that occurred. Restarts are usually put in places in the program where you can somehow fix the error that occurred. They are usually run from exception handlers (see below).
- restart-case - sets "restart", culminating in promotion of the stack.
- find-restart - finds “restart” by name.
- invoke-restart - finds a "restart" by name and launches it.
- compute-restarts - calculates a list of all “restarts” installed in the current dynamic environment.
- unwind-protect — executes the operator’s body block, and after — regardless of whether the execution completed in a normal way, or through forced stack promotion — performs all the specified “protecting” blocks (functions).
In more detail about these, and other primitives connected with exception handling, it is possible to read in Peter Sibel's remarkable book "Practical Common Lisp", in chapter 19:
lisper.ru/pcl/beyond-exception-handling-conditions-and-restartsAll of our implementation will be contained in the static class
Conditions . Further I will describe his methods.
But first you should describe a pair of static variables.
In each program execution thread, exception handlers and restarts during installation form a stack. Generally, formally speaking, the stack forms the dynamic environments of each thread, but since dynamic environments in C #, strictly speaking, are absent, we will “hands” connect the data structure with a “stack” with each thread.
static ConditionalWeakTable<Thread, Stack<Tuple<Type, HandlerBindCallback>>> _handlerStacks; static ConditionalWeakTable<Thread, Stack<Tuple<string, RestartBindCallback>>> _restartStacks; static Conditions() { _handlerStacks = new ConditionalWeakTable<Thread, Stack<Tuple<Type, HandlerBindCallback>>>(); _restartStacks = new ConditionalWeakTable<Thread, Stack<Tuple<string, RestartBindCallback>>>(); }
For the “thread -> stack” dictionary, I chose the ConditionalWeakTable class added in .NET 4.0, but you can use any other similar data structure. ConditionalWeakTable is good because it is a hash label with “weak pointers” (WeakPointer - hence the Weak in the class name) on the keys, and this, respectively, means that when you remove a thread object from the garbage collector, we will not leak of memory.
Exception Handlers and Signaling
Handlerbind
public static T HandlerBind<T>(Type exceptionType, HandlerBindCallback handler, HandlerBody<T> body) { if (null == exceptionType) throw new ArgumentNullException("exceptionType"); if (!exceptionType.IsSubclassOf(typeof(Exception))) throw new InvalidOperationException("exceptionType is not a subtype of System.Exception"); if (null == handler) throw new ArgumentNullException("handler"); if (null == body) throw new ArgumentNullException("body"); Thread currentThread = Thread.CurrentThread; var clusters = _handlerStacks.GetOrCreateValue(currentThread); clusters.Push(Tuple.Create(exceptionType, handler)); try { return body(); } finally { clusters.Pop(); } }
The HandlerBind method takes three parameters in us - the type of exception that the handler is associated with (as can be seen from the body of the method, it must be a subclass of Exception), the callback defining the handler code, and another delegate defining the code executed in the operator's body.
The types of delegates handler and body are:
public delegate void HandlerBindCallback(Exception exception); public delegate T HandlerBody<T>();
The exception parameter passed to the handler in the arguments is the exception object itself.
As you can see, the implementation of HandlerBind is simple - we add a new one to the stack of handlers associated with the current thread, then execute the operator's body code, and finally, in the body of finally, remove the handler from the stack. Thus, the stack of exception handlers is associated with the stack of execution of the current thread, and each installed handler becomes invalid when it leaves the corresponding stack frame of the program execution thread.
Handlercase
public static T HandlerCase<T>(Type exceptionType, HandlerCaseCallback<T> handler, HandlerBody<T> body) { if (null == exceptionType) throw new ArgumentNullException("exceptionType"); if (!exceptionType.IsSubclassOf(typeof(Exception))) throw new InvalidOperationException("exceptionType is not a subtype of System.Exception"); if (null == handler) throw new ArgumentNullException("handler"); if (null == body) throw new ArgumentNullException("body"); var unwindTag = new UnwindTag<T>(); HandlerBindCallback handlerCallback = (e) => { unwindTag.Value = handler(e); throw unwindTag; }; try { return HandlerBind(exceptionType, handlerCallback, body); } catch (UnwindTag<T> e) { if (e == unwindTag) { return e.Value; } else throw; } }
The implementation of HandlerCase is somewhat more complicated. The difference from HandlerBind, I remind you, is that this operator spins the stack to the point where the handler is installed. Since explicit escaping continuations are not allowed in C # (that is, roughly, we cannot make a goto or return from the lambda sent down the stack to an external block), we use regular try-catch to unwind the stack, and we identify the handler block
UnwindTag helper class
object class UnwindTag<T> : Exception { public T Value { get; set; } }
HandlerCaseCallback differs from HandlerBindCallback only in that it returns a value:
public delegate T HandlerCaseCallback<T>(Exception exception);
Signalal
Signal is the heart of the CL exception handling system. Unlike throw and associates from other programming languages, it does not promote the call stack, but merely signals an exception that has occurred, that is, it simply calls a suitable handler.
public static void Signal<T>(T exception) where T : Exception { if (null == exception) throw new ArgumentNullException("exception"); Thread currentThread = Thread.CurrentThread; var clusters = _handlerStacks.GetOrCreateValue(currentThread); var i = clusters.GetEnumerator(); while (i.MoveNext()) { var type = i.Current.Item1; var handler = i.Current.Item2; if (type.IsInstanceOfType(exception)) { handler(exception); break; } } }
As you can see - everything is very simple. From the current stack of exception handlers, we take the first one that is able to work with the exception class, the instance of which is the object passed to us in the exception parameter.
Error
public static void Error<T>(T exception) where T : Exception { Signal(exception); throw exception; }
Error differs from Signal only in that it interrupts the normal flow of program execution if there is no suitable handler. If we were writing a full-fledged implementation of Common Lisp under .NET, instead of a “throw exception,” there would be something like “InvokeDebuggerOrDie (exception);”
Restarts
RestartBind and RestartCase
RestartBind and RestartCase are very similar to HandlerBind and HandlerCase, with the difference that they work with the restart stack, and assign the exception delegate to the handler delegate, but the string, the restart name.
public delegate object RestartBindCallback(object param); public delegate T RestartCaseCallback<T>(object param); public static T RestartBind<T>(string name, RestartBindCallback restart, HandlerBody<T> body) { if (null == name) throw new ArgumentNullException("name"); if (null == restart) throw new ArgumentNullException("restart"); if (null == body) throw new ArgumentNullException("body"); Thread currentThread = Thread.CurrentThread; var clusters = _restartStacks.GetOrCreateValue(currentThread); clusters.Push(Tuple.Create(name, restart)); try { return body(); } finally { clusters.Pop(); } } public static T RestartCase<T>(string name, RestartCaseCallback<T> restart, HandlerBody<T> body) { if (null == name) throw new ArgumentNullException("name"); if (null == restart) throw new ArgumentNullException("restart"); if (null == body) throw new ArgumentNullException("body"); var unwindTag = new UnwindTag<T>(); RestartBindCallback restartCallback = (param) => { unwindTag.Value = restart(param); throw unwindTag; }; try { return RestartBind(name, restartCallback, body); } catch (UnwindTag<T> e) { if (e == unwindTag) { return e.Value; } else throw; } }
FindRestart and InvokeRestart
FindRestart and InvokeRestart, in turn, are very similar to the Signal method - the first function finds a restart in the corresponding stack of the current thread by name, and the second not only finds it, but also starts it immediately.
public static RestartBindCallback FindRestart(string name, bool throwOnError) { if (null == name) throw new ArgumentNullException("name"); Thread currentThread = Thread.CurrentThread; var clusters = _restartStacks.GetOrCreateValue(currentThread); var i = clusters.GetEnumerator(); while (i.MoveNext()) { var restartName = i.Current.Item1; var restart = i.Current.Item2; if (name == restartName) return restart; } if (throwOnError) throw new RestartNotFoundException(name); else return null; } public static object InvokeRestart(string name, object param) { var restart = FindRestart(name, true); return restart(param); }
ComputeRestarts
ComputeRestarts simply returns a list of all the restarts currently set — this can be useful, for example, to an exception handler, so that when called, it can select the appropriate restart for a particular situation.
public static IEnumerable<Tuple<string, RestartBindCallback>> ComputeRestarts() { var restarts = new Dictionary<string, RestartBindCallback>(); Thread currentThread = Thread.CurrentThread; var clusters = _restartStacks.GetOrCreateValue(currentThread); return clusters.AsEnumerable(); }
UnwindProtect
Our implementation of UnwindProtect simply wraps a try-finally block.
public static T UnwindProtect<T>(HandlerBody<T> body, params Action[] actions) { if (null == body) throw new ArgumentNullException("body"); if (null == actions) actions = new Action[0]; try { return body(); } finally { foreach (var a in actions) a(); } }
Finally - a few examples of use.
- Use HandlerBind with an exception signaling function.
static int DivSignal(int x, int y) { if (0 == y) { Conditions.Signal(new DivideByZeroException()); return 0; } else return x / y; }
int r = Conditions.HandlerBind( typeof(DivideByZeroException), (e) => { Console.WriteLine("Entering handler callback"); }, () => { Console.WriteLine("Entering HandlerBind with DivSignal"); var rv = DivSignal(123, 0); Console.WriteLine("Returning {0} from body", rv); return rv; }); Console.WriteLine("Return value: {0}\n", r);
Here, the DivSignal function, when the divider is equal to zero, signals a situation that has arisen, but nevertheless, it itself “copes” with it (returns zero). In this case, neither the handler nor the function itself interrupts the normal course of the program.
The output to the console is as follows:
Entering HandlerBind with DivSignal Entering handler callback Returning 0 from body Return value: 0
- Using HandlerCase and UnwindProtect with an error signal via Error.
static int DivError(int x, int y) { if (0 == y) Conditions.Error(new DivideByZeroException()); return x / y; }
int r = Conditions.HandlerCase( typeof(DivideByZeroException), (e) => { Console.WriteLine("Entering handler callback"); Console.WriteLine("Returning 0 from handler"); return 0; }, () => { Console.WriteLine("Entering HandlerCase with DivError and UnwindProtect"); return Conditions.UnwindProtect( () => { Console.WriteLine("Entering UnwindProtect"); var rv = DivError(123, 0); Console.WriteLine("This line should not be printed"); return rv; }, () => { Console.WriteLine("UnwindProtect exit point"); }); }); Console.WriteLine("Return value: {0}\n", r);
In this case, the DivError function throws an exception, but the handler intercepts it, spins the stack, and returns its value (in this case, 0). In the course of stack promotion, the computation flow passes through UnwindProtect.
This example, unlike the others, could be rewritten using ordinary try, catch and finally.
Output to console:
Entering HandlerCase with DivError and UnwindProtect Entering UnwindProtect Entering handler callback Returning 0 from handler UnwindProtect exit point Return value: 0
- Use HandlerBind with the function in which the restart is set.
static int DivRestart(int x, int y) { return Conditions.RestartCase( "ReturnValue", (param) => { Console.WriteLine("Entering restart ReturnValue"); Console.WriteLine("Returning {0} from restart", param); return (int)param; }, () => { Console.WriteLine("Entering RestartCase"); return DivError(x, y); }); }
DivRestart sets up a restart with the name “ReturnValue”, which, when activated, simply returns the value passed to it through the (param) parameter. The body RestartCase calls the DivError described in the previous example.
int r = Conditions.HandlerBind( typeof(DivideByZeroException), (e) => { Console.WriteLine("Entering handler callback"); Console.WriteLine("Invoking restart ReturnValue with param = 0"); Conditions.InvokeRestart("ReturnValue", 0); }, () => { Console.WriteLine("Entering HandlerBind with DivRestart"); return DivRestart(123, 0); }); Console.WriteLine("Return value: {0}", r);
The handler installed in HandlerBind, when called, searches for the restart of “ReturnValue” and sends it the number 0 to the parameter, then “ReturnValue” is activated, unwinds the stack to its level, and returns this same number from the RestartCase installed in DivRestart, as seen above.
Conclusion:
Entering HandlerBind with DivRestart Entering RestartCase Entering handler callback Invoking restart ReturnValue with param = 0 Entering restart ReturnValue Returning 0 from restart Return value: 0
The full source code for the library and examples is available on github:
github.com/Lovesan/ConditionSystem