Eleanor Holley
9 Feb 2021
•
9 min read
I have written previously on maybe monads and how to use them with lists to eliminate the possibility of null references in an object-oriented programming language. This standalone post walks through how to use a more generalized kind of monad to prevent all other kinds of unhandled exceptions, using data parsing exceptions as an example.
(Note: I had originally planned to include this in the previous series, but ultimately found the information difficult to organize from that perspective. Presenting this as a standalone article allows the reader to write a try monad from scratch without starting from knowing anything about the previous maybe monad, but an unfortunate side effect is that this post may feel somewhat repetitive after the previous two posts.)
Exception handling in procedural code is both awkward and unreliable. It's awkward like other block-level programming concepts--very verbose but also not very easy to read. It's also unreliable because it can be difficult to predict which operations will throw exceptions that need to be handled.
The Java compiler provides some help through checked exceptions, and checked exceptions make for lengthy, awkward method signatures, but making an exception a checked exception is optional, so there are still no guarantees.
For those reasons, C# avoids checked exceptions altogether, but this makes execution unpredictable because without looking at the source, you have no way of knowing for sure which C# functions contain unsafe operations. Even if the exceptions are very well documented, the compiler does nothing to ensure that you handle exceptions properly. Hence, we get absurd constructs like TryParse
which require both an out parameter and a null check.
/* SO MANY LINES OF CODE */
bool success = Int32.TryParse(value, out number);
if (success)
{
Console.WriteLine("Converted '{0}' to {1}.", value, number);
}
else
{
Console.WriteLine("Attempted conversion of '{0}' failed.", value ?? "<null>");
}
So object-oriented programmers generally just accept that sometimes their code throws unexpected exceptions, even if you try very hard to handle them all. This post will show you that you do not have to accept unexpected program behavior. It is very possible to neatly handle every exception by introducing functional constructs into your object-oriented code.
Monads provide a wrapper around a value that may or may not exist. We can decide to handle the exception specifically or fail silently, and our choice will be concise, explicit, and readable.
In functional programming, monads are union types. Object-oriented languages do not have union types per se, but they do have interfaces which can be equivalent to union types. Interfaces are also an object-oriented best-practice for hiding details, so we know we're getting the best of both worlds.
Let's start with our empty interface. This will represent both possible states: either we have our value, or we have an exception.
Try.cs
public interface ITry<T>
{
}
Now we can implement it with our two classes. The Success
type actually contains the data.
Success.cs
public class Success<T>
{
private T member;
public Success(T member) { this.member = member; }
}
Then, our failure type contains only an exception.
Failure.cs
public class Failure<T>
{
private Exception ex;
public Failure(Exception ex) { this.ex = ex; }
}
Now that we've defined both constructors, we can write our error-trapping code.
Try.cs
public interface ITry<T>
{
}
public static class Try
{
public ITry<T> Factory<T>(Func<T> unsafeOperation)
{
try { return new Success<T>(unsafeOperation()) }
catch(Exception e) { return new Failure(e) }
}
}
Now, we can use this Factory
to wrap our methods that might throw exceptions and return instances of ITry<T>
instead of T
, which will
ITry<int> int n = Try.Factory<int>(() => int.Parse("hello, world."));
Now, though, we need a way of interacting with the returned value if the operation was successful. member
is private to Success
, but we can do this by passing functions. Let's add two methods to the interface like so:
Try.cs
public interface ITry<T>
{
/// <summary> applies func to the value if `this` was a `Success`, else
/// fails silently
/// </summary>
/// <returns> a new `Success` of the result of `func(t)` or a Failure
/// </returns>
ITry<TNext> Map<TNext>(Func<T, TNext> func);
/// <summary> applies func if `this` was a `Success` or fails silently.
/// </summary>
/// <returns> a new `Success` if both `this` and `func` were successful
/// or a failure if either failed.
/// </returns>
ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func);
}
public static class Try
{
public ITry<T> Factory<T>(Func<T> unsafeOperation)
{
try { return new Success<T>(unsafeOperation()) }
catch(Exception e) { return new Failure<T>(e) }
}
}
This allows us to use the value if we possibly can while carrying forward our protective cover on the value.
Now we can implement them safely like so:
Success.cs
public class Success<T>
{
private T member;
public Success(T member) { this.member = member; }
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> Try.Factory(() => func(member));
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> func(member);
}
Failure.cs
public class Failure<T>
{
private Exception ex;
public Failure(Exception ex) { this.ex = ex; }
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> new Failure<TNext>(ex);
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> new Falure<TNext>(ex);
}
If you're familiar with object-oriented design patterns, you might recognize this as the null-object pattern--failures are represented by a dummy object that silently ignores its methods, like this:
ITry<int> doubled = Try.Factory<int>(() => int.Parse("5"))
.Map(i => i * 2);
This is all well and good, but at a certain point, we may need to unwrap our member value. There are two ways of doing so safely. Let's look at both of those now.
We can unwrap our ITry<T>
and get a T
as long as we provide a fallback.The most obvious way of doing so is by providing the value directly, through a method called GetSafe()
.
Try.cs
public interface ITry<T>
{
/// <returns> the member of `this` if `this` was a `Success`, or the
/// fallback if it was a `Failure`.
/// </returns>
T GetSafe(T fallback);
/// <summary> applies func to the value if `this` was a `Success`, else
/// fails silently
/// </summary>
/// <returns> a new `Success` of the result of `func(t)` or a Failure
/// </returns>
ITry<TNext> Map<TNext>(Func<T, TNext> func);
/// <summary> applies func if `this` was a `Success` or fails silently.
/// </summary>
/// <returns> a new `Success` if both `this` and `func` were successful
/// or a failure if either failed.
/// </returns>
ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func);
}
public static class Try
{
public ITry<T> Factory<T>(Func<T> unsafeOperation)
{
try { return new Success<T>(unsafeOperation()) }
catch(Exception e) { return new Failure<T>(e) }
}
}
Success.cs
public class Success<T>
{
private T member;
public Success(T member) { this.member = member; }
public T GetSafe(T fallback) => member;
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> Try.Factory(() => func(member));
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> func(member);
}
Failure.cs
public class Failure<T>
{
private Exception ex;
public Failure(Exception ex) { this.ex = ex; }
public T GetSafe(T fallback) => fallback;
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> new Failure<TNext>(ex);
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> new Falure<TNext>(ex);
}
This is straightforward--Success
discards the fallback and Failure
relies on it. This gives us error trapping and recovery in a single line, like this:
//evaluates to zero.
int i = Try.Factory<int>(() => int.Parse("hello, world"))
.GetSafe(0);
Suppose, however, that the fallback value was computationally expensive to evaluate or retrieve. We would not want to compute that value and discard it every time an operation succeeded. We can instead compute it conditionally using functional programming.
Let's call our new method Match
, like pattern matching constructs in functional programming. This function will take two function parameters and execute the appropriate one.
Try.cs
public interface ITry<T>
{
/// <returns> the member of `this` if `this` was a `Success`, or the
/// fallback if it was a `Failure`.
/// </returns>
T GetSafe(T fallback);
/// <summary> applies func to the value if `this` was a `Success`, else
/// fails silently
/// </summary>
/// <returns> a new `Success` of the result of `func(t)` or a Failure
/// </returns>
ITry<TNext> Map<TNext>(Func<T, TNext> func);
/// <summary> applies func if `this` was a `Success` or fails silently.
/// </summary>
/// <returns> a new `Success` if both `this` and `func` were successful
/// or a failure if either failed.
/// </returns>
ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func);
/// <summary>applies `success` if `this was a `Success` or applies
/// `failure` if `this` was a failure.
/// </summary>
/// <returns> the result of whichever function executed.</returns>
TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure);
}
public static class Try
{
public ITry<T> Factory<T>(Func<T> unsafeOperation)
{
try { return new Success<T>(unsafeOperation()) }
catch(Exception e) { return new Failure<T>(e) }
}
}
Now we can implement the function by applying the appropriate function and discarding the other in each interface.
Success.cs
public class Success<T>
{
private T member;
public Success(T member) { this.member = member; }
public T GetSafe(T fallback) => member;
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> Try.Factory(() => func(member));
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> func(member);
public TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure)
=> success(member);
}
Failure.cs
public class Failure<T>
{
private Exception ex;
public Failure(Exception ex) { this.ex = ex; }
public T GetSafe(T fallback) => fallback;
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> new Failure<TNext>(ex);
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> new Falure<TNext>(ex);
public TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure)
=> failure(ex);
}
Note that the second function, failure
, allows us to interact with the exception itself, so we can do things like log its stack trace or send alerts just like we would in a try
/catch
block.
/* The long running calculation doesn't execute as long as the parse
* succeeds.*/
int n = Try.Factory<int>(() => int.Parse("5"))
.Match(i => i, ex => longRunningCalculation());
Of course, there are some errors from which we cannot or should not try to recover. For example, if the database becomes unresponsive, the right thing for a back-end service to do is to throw an exception so the framework will respond to the front-end with a 500-level HTTP response. For this reason, it is a good idea to provide an escape hatch, which we'll call GetUnsafe()
.
Try.cs
public interface ITry<T>
{
/// <returns> the member of `this` if `this` was a `Success`, or the
/// fallback if it was a `Failure`.
/// </returns>
T GetSafe(T fallback);
/// <returns> the member of `this` if `this was a `Success` or throws
/// the exception if `this` was a failure.
/// </returns>
T GetUnsafe();
/// <summary> applies func to the value if `this` was a `Success`, else
/// fails silently
/// </summary>
/// <returns> a new `Success` of the result of `func(t)` or a Failure
/// </returns>
ITry<TNext> Map<TNext>(Func<T, TNext> func);
/// <summary> applies func if `this` was a `Success` or fails silently.
/// </summary>
/// <returns> a new `Success` if both `this` and `func` were successful
/// or a failure if either failed.
/// </returns>
ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func);
/// <summary>applies `success` if `this was a `Success` or applies
/// `failure` if `this` was a failure.
/// </summary>
/// <returns> the result of whichever function executed.</returns>
TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure);
}
public static class Try
{
public ITry<T> Factory<T>(Func<T> unsafeOperation)
{
try { return new Success<T>(unsafeOperation()) }
catch(Exception e) { return new Failure<T>(e) }
}
}
Success.cs
public class Success<T>
{
private T member;
public Success(T member) { this.member = member; }
public T GetSafe(T fallback) => member;
public T GetUnsafe() => member;
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> Try.Factory(() => func(member));
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> func(member);
public TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure)
=> success(member);
}
Failure.cs
public class Failure<T>
{
private Exception ex;
public Failure(Exception ex) { this.ex = ex; }
public T GetSafe(T fallback) => fallback;
public T GetUnsafe()
=> throw new Exception("GetUnsafe failure.", ex);
public ITry<TNext> Map<TNext>(Func<T, TNext> func)
=> new Failure<TNext>(ex);
public ITry<TNext> FlatMap<TNext>(Func<T, ITry<TNext>> func)
=> new Falure<TNext>(ex);
public TNext Match(Func<T, TNext> success, Func<Exception, TNext> failure)
=> failure(ex);
}
(Side note: don't just rethrow the old ex
without creating a new exception--rethrowing the old will destroy important information in the stack trace you'llwant to preserve for logging.)
It's worth asking: what's the point of trapping exceptions if we're just going to throw them again? Wouldn't it be better in these cases to let the exceptions bubble up?
I bring this up to highlight the importance of readability. Unsafe code should, at the very least, always come with a clear warning label. Throwing an exception should never be an accident or a surprise, and if the calling code needs to throw an exception, it should be required to do so with a function called GetUnsafe
.
Adopting a few functional practices can make your object-oriented life a whole lot easier. If you like generic, highly abstract, extremely safe code like this, keep studying functional programming and don't let object-orientation hold you back. If you'd like to stay on this path with me, feel free to subscribe to my RSS feed, and in return I promise not to write a single post this long ever again.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!