Safer Data Parsing with Try Monads
csharp monadI 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.)
The need for safer exception handling
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, but checked exceptions make for lengthy, awkward method signatures, and 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.
The appeal of monads as a solution.
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.
Modeling possible states
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
- improve the readability of our code by clearly communicating which methods are unsafe and
- force the calling code to decide how to handle the exception case.
- compress our error handling into a single readable line like so:
ITry<int> int n = Try.Factory<int>(() => int.Parse("hello, world."));
Mapping and FlatMapping
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.
Safely unwrapping a try monad using a fallback
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);
Functional fallbacks mimicking pattern matching
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 C#. 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());
Rethrowing the exception
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'll
want to preserve for logging.)
But why tho?
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
.
In conclusion
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.
I write to learn, so I welcome your constructive criticism. Report issues on GitLab.