Continuing on from yesterday’s post about simplifying exception handling code by using lambdas and continuation-passing style, I’d like to show off some other examples. Another common pattern in code is to generate a value, test it against some condition, and then have an if/else block to execute two other blocks of code dependent on that test.
So, let’s introduce the TryIgnore() and GenerateTest() helper functions with an example (see below for the trivial implementation). I want to generate a full file path for a log file, delete it if it exists, and then enable logging into that file for the later Windows Installer stuff that will be happening. However, if any of that fails I do not want an exception to be propagated because it isn’t critical to the success of what I’m doing.
Here’s the old code: (I’m omitting the code that initializes the tempPath, dwMsiLogMode, and dwMsiLogAttributes values. Assume that tempPath is just "%TEMP%", and the others aren’t really important for this discussion)
const string logFileName = "PdnMsiInstall.log";
try
{
string logFilePath = Path.Combine(tempPath, logFileName);if (File.Exists(logFilePath))
{
File.Delete(logFilePath);
}
}catch (Exception)
{
// Ignore error
}try
{
NativeMethods.MsiEnableLogW(
dwMsiLogMode,
logFilePath,
dwMsiLogAttributes);
}catch (Exception)
{
// Ignore error
}
And the new code, which makes use of the TryIgnore() and GenerateTest() helper methods:
const string logFileName = "PdnMsiInstall.log";
Do.TryIgnore(() =>
Do.GenerateTest(() =>
Path.Combine(tempPath, logFileName),
s => File.Exists(s),
s => File.Delete(s),
s => { /* no-op */ }));Do.TryIgnore(() => NativeMethods.MsiEnableLogW(dwMsiLogMode, logFilePath, dwMsiLogAttributes));
There’s room to make the code even more succinct by inlining the value of logFileName into the Path.Combine() call, and to have an overload of GenerateTest() that doesn’t take an "ifFalse" delegate. Honestly though, I prefer to have my constants sitting in named values. It makes things easier when you’re sitting in the debugger and scratching your head and/or chin.
Here is the full code for my current "Do" class. I hereby release this into the public domain, so do whatever you want with it. There are obviously many other helper functions that could be added, but what I have her is sufficient to get things started. I’ll let you add others as you need them. And remember, my definition of Function has the type parameters in the reverse order that .NET 3.5’s Func delegate has them.
public delegate R Function<R>();
public delegate R Function<R, TArg>(TArg arg);
public delegate void Procedure();
public delegate void Procedure<TArg>(TArg arg);public static class Do
{
public static void GenerateTest<T>(
Function<T> generate,
Function<bool, T> test,
Procedure<T> ifTrue,
Procedure<T> ifFalse)
{
T value = generate();
(test(value) ? ifTrue : ifFalse)(value);
}public static bool TryBool(Procedure actionProcedure)
{
try
{
actionProcedure();
return true;
}catch (Exception)
{
return false;
}
}public static Exception TryEx(Procedure actionProcedure)
{
try
{
actionProcedure();
return null;
}catch (Exception ex)
{
return ex;
}
}public static void TryIgnore(Procedure actionProcedure)
{
try
{
actionProcedure();
}catch (Exception)
{
// Ignore
}
}public static void TryCatch(
Procedure actionProcedure,
Procedure<Exception> catchClause)
{
try
{
actionProcedure();
}catch (Exception ex)
{
catchClause(ex);
}
}public static T TryCatch<T>(
Function<T> actionFunction,
Function<T, Exception> catchClause)
{
T returnVal;try
{
returnVal = actionFunction();
}catch (Exception ex)
{
returnVal = catchClause(ex);
}return returnVal;
}public static void TryCatchFinally(
Procedure actionProcedure,
Procedure<Exception> catchClause,
Procedure finallyClause)
{
try
{
actionProcedure();
}catch (Exception ex)
{
catchClause(ex);
}finally
{
finallyClause();
}
}public static T TryCatchFinally<T>(
Function<T> actionFunction,
Function<T, Exception> catchClause,
Procedure finallyClause)
{
T returnVal;try
{
returnVal = actionFunction();
}catch (Exception ex)
{
returnVal = catchClause(ex);
}finally
{
finallyClause();
}return returnVal;
}public static void TryFinally(
Procedure actionProcedure,
Procedure finallyClause)
{
try
{
actionProcedure();
}finally
{
finallyClause();
}
}
}
What is the point of passing the “generate” delegate to GenerateTest if it’s going to be evaluated right away? (it could just as well be a regular parameter of type T)
Why is this GenerateTest needed at all when you can do this:
Do.TryIgnore(delegate()
{
string logFilePath = Path.Combine(tempPath, logFileName);
if (File.Exists(logFilePath)) File.Delete(logFilePath);
});
Also, allowing null delegates to be passed for no-ops has less overhead and seems logical.
Pent, it’s an exercise in continuation-passing style. Writing the code this way is sometimes just more natural to do than to have an if/else block and to store values in intermediate, explicitely declared variables.
Passing a null delegate makes sense. An overload that has 1 less parameter could also work well.
Also, Pent, part of what I’m trying to do here is get more discussion and thought around CPS, as I think it’s a powerful but underutilized tool in our traditional imperative languages. I’ve only been playing with it for a few days, and I’m interested to see what other people think about it and if they have better/cooler ways of doing things.
The only thing I don’t like about is that it’s not clear what the “new code” actually *does* without already having an understanding of the GenerateTest and TryIgnore methods. In fact, it seems to me that after a couple of weeks of not looking at the code, if I had to come back to fix a bug, I’d spend more time trying to figure out what it was doing than actually typing out the “old code” in the first place…
It seems like a nifty trick, but without making it somehow self-documenting, it doesn’t seem like something I personally would use. You could probably add comments to explain it, but then is it really all that much different from just writing it all out by hand?
Dean, true. The trick is to find a balance between being able to use new constructs fluently, and never introducing any new constructs because they’re too hard to remember. If you use them often enough because they’re useful, then you (hopefully) won’t forget them.
Remember, there was a time where we didn’t have “for” and had to use cmp/jnz instead š (those are assembly language instructions)
True enough. And I guess on a one-man project such as Paint.NET it’s easier to keep up š
This looks like this could be quite a useful class but I was wondering how this might work when it comes to wanting to catch specific exceptions rather than just the base Exception type?
I know from using FxCop that using ‘catch (Exception)’ is something Paint.net rather a lot and while I’m sure you sleep at night doing that, its something I’m trying to avoid in my own app. š Are there any simple ways this code could be tweaked to not use Exception?