DEV Community

webbureaucrat
webbureaucrat

Posted on • Originally published at webbureaucrat.gitlab.io on

Safer Data Parsing with Try Monads

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.)

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, 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>");
}
Enter fullscreen mode Exit fullscreen mode

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>
{
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

Then, our failure type contains only an exception.

Failure.cs

public class Failure<T>
{ 
    private Exception ex; 
    public Failure(Exception ex) { this.ex = ex; }
}
Enter fullscreen mode Exit fullscreen mode

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) } 
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. improve the readability of our code by clearly communicating which methods are unsafe and
  2. force the calling code to decide how to handle the exception case.
  3. compress our error handling into a single readable line like so:
ITry<int> int n = Try.Factory<int>(() => int.Parse("hello, world."));
Enter fullscreen mode Exit fullscreen mode

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) } 
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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) } 
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

This is straightforward--Success discards the fallback and Failurerelies 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);
Enter fullscreen mode Exit fullscreen mode

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 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) } 
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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) } 
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

(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.)

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.

Top comments (1)

Collapse
 
kallmanation profile image
Nathan Kallman

I feel like I understand Monad's better than ever; thanks for this article!