The implementation of the Common Lisp Condition System in C#
One of the most remarkable and attractive properties of the Common Lisp language is, of course, his system of exception handling.
Moreover, in my personally opinion, this approach to exceptions is only correct for all imperative languages, and here for some simple reason:
The mechanism of "exceptions"(or as they are called in the world CL — conditions) in Common Lisp is separated from the mechanism to unwind the stack, and this, consequently, allows you to handle any pop-up in the program exceptional(exceptional) situation right in the place where they originated, without losing the execution context of the program, which entails the ease of development, debugging, and generally, convenience of building a program logic.
Perhaps we should say that Common Lisp Condition System, despite its uniqueness in the environment of high-level programming languages, is very close to many famous developers low-level means of modern operating systems, namely synchronous signals UNIX and, much closer to the mechanism of SEH(Structured Exception Handling) from Windows. Leading the implementation of CL base controls the flow of calculations as the mechanism of exception handling and stack unwinding on them.
Despite the lack of a similar mechanism in many other(if not all) imperative programming languages, it lends itself to realization in a more or less sane views on most of them. In this article I will describe the implementation in C# along the way, examining in detail the very concept of this approach to "exceptions".
For the complete implementation of CLCS from a programming language, but rather from the runtime, you need the following few things:
the
We will bring C# the following primitives processing system of exceptions of CL:
Read more about these and other entities associated with the exception handling, can be read in the wonderful book Peter Sibel "Practical Common Lisp", Chapter 19:
lisper.ru/pcl/beyond-exception-handling-conditions-and-restarts
All implementation will be contained in a static class Conditions. Next, I will describe its methods.
But first you should describe a couple of static variables.
Each thread of execution of a program in exception handlers and restarts when you install form the stack. Actually, technically speaking, the stack form a dynamic environment of each thread, but as a dynamic environment in C#, strictly speaking, no, we are going to associate with each tridom data structure "stack".
the
For vocabulary "thread -> stack" here I chose the ConditionalWeakTable class added in the .NET 4.0, but you can use any other similar data structure. ConditionalWeakTable is good because it is a hash table with a "weak pointers"(WeakPointer — hence the Weak in the name of the class) on the keys and this, accordingly, means that when you delete an object of a thread(Thread) a garbage collector, we will not have memory leaks.
the
We HandlerBind method accepts three parameters: the exception type associated with the handler(as can be seen from the method body, it needs to be a subclass of Exception), callback that specifies the handler code, and another delegate that defines the code executed in the body of the operator.
The handler delegate types, and body such as:
the
The exception parameter passed to the handler in the argument is the exception object.
As can be seen, the implementation of HandlerBind simple to the stack of handlers associated with the current tridom, we add new after — executable code of the operator's body, and eventually, in the body finally, we remove the handler from the stack. Thus, the stack of exception handlers associated with the stack of the current thread, and each installed handler becomes invalid when you exit from the corresponding stack frame of the program execution.
the
Implementation HandlerCase more complicated. Contrast HandlerBind recall that the operator unwinds the stack to the point at which the handler is installed. Since C# disallows explicit escaping continuations(that is, roughly speaking, we can't do a goto or return from the lambda that is passed down the stack, in the external unit), then to unwind the stack, we use a normal try-catch, and the handler block can identify the object of the helper class, UnwindTag
the
HandlerCaseCallback different from HandlerBindCallback only in that it returns a value:
the
The Signal function is the heart of processing system of exceptions of CL. Unlike throw and associates from other programming languages, it does not spin call stack, but only signals about the occurred exception, that is simply calls the appropriate handler.
the
As you can see — all very simply. From the current stack of exception handlers we take the first capable of working with the exception class, an instance of which is the object passed to us in the exception parameter.
the
Error is different from the Signal only in that interrupts the normal flow of program execution in the absence of a suitable handler. If we wrote a complete implementation of Common Lisp .NET, instead of "throw exception" would be something like "InvokeDebuggerOrDie(exception);"
RestartBind and RestartCase very similar to HandlerBind and HandlerCase, with the difference that working with a stack restarts, and assign a delegate to handler the exception type and the string name of the restart.
the
FindRestart and InvokeRestart, in turn, very similar to the method Signal, the first function finds the restart in the corresponding stack of the current thread by name, and the second one not only finds it but starts immediately.
the
ComputeRestarts simply returns a list of all installed at the moment restarts — this may be useful, for example, the exception handler when the call can choose the appropriate restart for a particular situation.
the
Our implementation UnwindProtect just wraps a try-finally block.
the
Finally — some examples of usage.
The complete source code libraries and examples available on github: github.com/Lovesan/ConditionSystem
Article based on information from habrahabr.ru
Moreover, in my personally opinion, this approach to exceptions is only correct for all imperative languages, and here for some simple reason:
The mechanism of "exceptions"(or as they are called in the world CL — conditions) in Common Lisp is separated from the mechanism to unwind the stack, and this, consequently, allows you to handle any pop-up in the program exceptional(exceptional) situation right in the place where they originated, without losing the execution context of the program, which entails the ease of development, debugging, and generally, convenience of building a program logic.
Perhaps we should say that Common Lisp Condition System, despite its uniqueness in the environment of high-level programming languages, is very close to many famous developers low-level means of modern operating systems, namely synchronous signals UNIX and, much closer to the mechanism of SEH(Structured Exception Handling) from Windows. Leading the implementation of CL base controls the flow of calculations as the mechanism of exception handling and stack unwinding on them.
Despite the lack of a similar mechanism in many other(if not all) imperative programming languages, it lends itself to realization in a more or less sane views on most of them. In this article I will describe the implementation in C# along the way, examining in detail the very concept of this approach to "exceptions".
For the complete implementation of CLCS from a programming language, but rather from the runtime, you need the following few things:
the
-
the
- Strict stack model of execution. Here I mean the absence of language to the full "continuations"(continuations). This item is fairly conventional, but as continue contribute to the flow-control computing is a huge blur, and do not allow with sufficient accuracy to define the basic primitives, which pushes CLCS, their presence is highly undesirable. the
- higher-order Functions, anonymous functions, and closures. of Course, if you try, everything is possible to implement using objects and classes, but in this case, to enjoy all this, in my opinion, would be extremely inconvenient. the
- Dynamic environment and, in particular, dynamic variables. About the dynamic environment and variables I more or less wrote extensively in his article on the semantics of modern Lisp: love5an.livejournal.com/371169.html
In the absence of similar concepts in a programming language, it is emulated with the following two items:
the - Operators try, catch and throw, or equivalents. These operators in any programming language that supports exceptions. the
- Primitive-UNWIND-PROTECT, or the equivalent(the try-finally block, RAII, etc.).
We will bring C# the following primitives processing system of exceptions of CL:
-
the
- handler-bind — sets an exception handler on runtime of a body of the operator. When catching exception handler may decide about the promotion of a stack, but is not obliged to do so. the
- handler-case — sets an exception handler on runtime of a body of the operator. While catching exception is unwinding the stack and the operator returns the value computed in the body of the handler. the
- signal — indicates an exception to the higher handler, if present. the
- error — indicates an exception to the higher handler, and in the case of the absence thereof or in the event of failure all of the handlers to handle the exception — throws an exception the usual method, i.e., a throw statement(It in our implementation. In Common Lisp by the error function calls the debugger, if one is connected, or otherwise terminates a separate calculation thread(thread) or the entire Lisp system.) the
- restart-case — sets "restart", ending with stack unwinding. the
- find-restart — finds "restart" by name. the
- invoke-restart — finds "restart" by name and runs it. the
- compute-restarts — calculates the list of all installed in the current dynamic environment "restarts". the
- unwind-protect — executes the block statement body, and then regardless of, whether ended the execution normally, or through forced unwinding the stack — performs all of these "protection" blocks(functions).
Read more about these and other entities associated with the exception handling, can be read in the wonderful book Peter Sibel "Practical Common Lisp", Chapter 19:
lisper.ru/pcl/beyond-exception-handling-conditions-and-restarts
All implementation will be contained in a static class Conditions. Next, I will describe its methods.
But first you should describe a couple of static variables.
Each thread of execution of a program in exception handlers and restarts when you install form the stack. Actually, technically speaking, the stack form a dynamic environment of each thread, but as a dynamic environment in C#, strictly speaking, no, we are going to associate with each tridom data structure "stack".
the
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 vocabulary "thread -> stack" here I chose the ConditionalWeakTable class added in the .NET 4.0, but you can use any other similar data structure. ConditionalWeakTable is good because it is a hash table with a "weak pointers"(WeakPointer — hence the Weak in the name of the class) on the keys and this, accordingly, means that when you delete an object of a thread(Thread) a garbage collector, we will not have memory leaks.
Handlers and signaling exceptions
HandlerBind
the
public static HandlerBind T<T>(Type exceptionType, handler HandlerBindCallback, 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();
}
}
We HandlerBind method accepts three parameters: the exception type associated with the handler(as can be seen from the method body, it needs to be a subclass of Exception), callback that specifies the handler code, and another delegate that defines the code executed in the body of the operator.
The handler delegate types, and body such as:
the
public delegate void HandlerBindCallback(Exception exception);
public delegate T HandlerBody<T>();
The exception parameter passed to the handler in the argument is the exception object.
As can be seen, the implementation of HandlerBind simple to the stack of handlers associated with the current tridom, we add new after — executable code of the operator's body, and eventually, in the body finally, we remove the handler from the stack. Thus, the stack of exception handlers associated with the stack of the current thread, and each installed handler becomes invalid when you exit from the corresponding stack frame of the program execution.
HandlerCase
the
public static HandlerCase T<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;
}
}
Implementation HandlerCase more complicated. Contrast HandlerBind recall that the operator unwinds the stack to the point at which the handler is installed. Since C# disallows explicit escaping continuations(that is, roughly speaking, we can't do a goto or return from the lambda that is passed down the stack, in the external unit), then to unwind the stack, we use a normal try-catch, and the handler block can identify the object of the helper class, UnwindTag
the
class UnwindTag<T> : Exception
{
public T Value { get; set; }
}
HandlerCaseCallback different from HandlerBindCallback only in that it returns a value:
the
public delegate T HandlerCaseCallback<T>(Exception exception);
Signal
The Signal function is the heart of processing system of exceptions of CL. Unlike throw and associates from other programming languages, it does not spin call stack, but only signals about the occurred exception, that is simply calls the appropriate handler.
the
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 — all very simply. From the current stack of exception handlers we take the first capable of working with the exception class, an instance of which is the object passed to us in the exception parameter.
Error
the
public static void Error<T>(T exception)
where T : Exception
{
Signal(exception);
throw exception;
}
Error is different from the Signal only in that interrupts the normal flow of program execution in the absence of a suitable handler. If we wrote a complete implementation of Common Lisp .NET, instead of "throw exception" would be something like "InvokeDebuggerOrDie(exception);"
Restarts
RestartBind and RestartCase
RestartBind and RestartCase very similar to HandlerBind and HandlerCase, with the difference that working with a stack restarts, and assign a delegate to handler the exception type and the string name of the restart.
the
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, very similar to the method Signal, the first function finds the restart in the corresponding stack of the current thread by name, and the second one not only finds it but starts immediately.
the
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 installed at the moment restarts — this may be useful, for example, the exception handler when the call can choose the appropriate restart for a particular situation.
the
public static IEnumerable<Tuple<string, RestartBindCallback>> ComputeRestarts()
{
var restarts = new Dictionary<string, RestartBindCallback>();
var clusters = _restartStacks.GetOrCreateValue(currentThread);
return clusters.AsEnumerable();
}
UnwindProtect
Our implementation UnwindProtect just wraps a try-finally block.
the
public static UnwindProtect T<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 — some examples of usage.
-
the
- Using HandlerBind function, signaling any exception.
thestatic int DivSignal(int x, int y) { if (0 == y) { Conditions.Signal(new DivideByZeroException()); return 0; } else return x / y; }
theint 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 function DivSignal, when the divisor is equal to zero, indicates a situation but nevertheless, she "copes" with it(returns zero). In this case, neither the handler nor the function itself does not interrupt the normal course of the program.
The console output is this:
theEntering HandlerBind with DivSignal Entering handler callback Returning 0 from body Return value: 0
the - Using HandlerCase and UnwindProtect with the function of signaling the error through an Error.
thestatic int DivError(int x, int y) { if (0 == y) Conditions.Error(new DivideByZeroException()); return x / y; }
theint 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 function throws an exception DivError, but the handler intercepts it, unwinds the stack, and returns its value(in this case 0). During stack unwinding, a thread of computation passes through UnwindProtect.
This example, unlike the others, could be rewritten using the usual try, catch and finally.
Console output:
theEntering HandlerCase with DivError and UnwindProtect Entering UnwindProtect Entering handler callback Returning 0 from handler UnwindProtect exit point Return value: 0
the - Using HandlerBind function with a restart.
thestatic 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 the restart with the name "ReturnValue", which, when activated, simply returns the value passed to it via parameter(param). Body RestartCase causes DivError described in the previous example.
theint 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 processor installed in HandlerBind, when you call looking for restart "ReturnValue" and passes it as a parameter the number 0 after the "ReturnValue" is activated, unwinds the stack to its level, and returns that same number of RestartCase installed in DivRestart as seen above.
Conclusion:
theEntering 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 complete source code libraries and examples available on github: github.com/Lovesan/ConditionSystem
Комментарии
Отправить комментарий