HiveBrain v1.2.0
Get Started
← Back to all entries
debugcsharpMajor

A Failable<T> that allows safe returning of exceptions

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
exceptionsallowsreturningthatsafefailable

Problem

This was inspired by a conversation in chat, that started with the discussion of C#7.0 tuples and out parameter declarations, which led to the idea that there is no 'good1' way to return an error state in C# without throwing an exception.

Out of curiosity, I wondered what it would take to design a type that was transparent to the developer, but allowed them to safely return exceptions without having to unwind the stack.

For those who don't know, when you throw/catch Exception objects in C# (or VB.NET, F#, any .NET language follows the same requirements), the most expensive part tends to be the stack. Throwing an exception is cheap, but catch the exception and the stack has to unwind and reflect against itself to give you the information you need. This is by-design, of course. The language and framework designers wanted exceptions to mean that the program entered an 'exceptional state', that is, there is an issue that needs resolved.

The problem is that some methods don't really need to throw an exception on error, they could, instead, just return a pass/fail and then fill an out parameter. The other option is to return a Tuple, where T is the return type.

Of course, this doesn't give us the ability to return an Exception, just pass/fail. Sometimes we may want to return what went wrong.

So, alas, I get to the Failable struct that I created today. By including implicit conversions to and from T and Exception, it allows us to simply return Exception instead of throwing, creating a much cheaper management of error states.

The only caveat to this approach from a usability standpoint, is that one does not simply define an implicit conversion from null. This means that Failable value = null; is invalid, but Failable value = (Failable)null; is, as well as Failable value = new Failable(null);.

If the framework/language designers ever open up implicit conversions from null, then this struct would be completely transparent.

```
pub

Solution

First: congratulations, you have rediscovered the error monad.

https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Error.html

Second: as noted in the comments, C# already has the concept of "wrap up either a value or an exception, namely, Task. You can use Task.FromException and Task.FromResult to construct them. Of course Result on the task either produces the result or throws the exception, as does await.

Also, this illustrates that there need be no asynchrony in a task! A task is just the concept of "I'll provide a value or an exception in the future if I don't already have it now". If you already have it now, great; you can use tasks to represent your "failable" concept, and await them like any other task.


Should there be a T(Failable) operator? If so, should it be implicit?

Operators that convert a generic type to anything whatsoever can be difficult to reason about. Certainly it should not be implicit because the operation is not guaranteed to succeed! If you want this, it should be explicit.

Look at the design of Task for inspiration here. Notice that the factories are static methods and they are very clear when they are being called. And it is also very clear when the result is being fetched.

Similarly look at the design for nullable. (The "maybe monad" is very similar to the error monad; more on this below.) There is an implicit conversion from T to T?, but the conversion from T? to T is explicit.


Should there be an Exception(Failable) operator? If so, should it be implicit?

I would find this confusing.


Should the API include a Failable(Tuple) constructor that allows one to pass a tuple of (pass, value)?

I don't understand the question. (Though I note that a (bool, T) tuple is the structure of the maybe monad, aka Nullable in C#.)

Exercise 1: you have created a monad, so you should be able to define the monad operators on them; if you do so, then you can use your type in LINQ queries! Can you implement members:

struct Failable ... {
  ...
  public Failable Select(Func f) { ... }
  public Failable SelectMany(
    Func> f1,
    Func f2)  { ... }
  public Failable Where(Func f) { ... }
}


If you do that, then you can write queries:

Failable f = OperationThatCanFail();
Failable d = from i in f where i > 0 select Math.log(i);


If you've done it right then d should be either a failure code, or the log of integer i.

Exercise 2: You've implemented the error monad; can you now implement the tracing monad? a Trace has the value of a T, but also has an operation that appends a string to the trace, so you can track the movement of T around your program.

Exercise 3: nullable is implemented as a (bool, T) pair. Failable is implemented as an (Exception, T) pair. Trace is implemented as a (string, T) pair. Can you design and implement a generalized State type which associates an S with a T, and then derive the other monads from it?

Finally, you might consider more advanced operations. For example:

public static Func> ToFailable(this Func f)
{
  return a => 
  {
    try 
    { 
      return new Failable(f(a));
    }
    catch(Exception x) 
    {
      return new Failable(x);
    }
  };
}


Now you can take existing functions of the form A-->R that can throw, and turn them into functions that cannot throw.

Code Snippets

struct Failable<T> ... {
  ...
  public Failable<T> Select<R>(Func<T, R> f) { ... }
  public Failable<C> SelectMany<B, C>(
    Func<T, Failable<B>> f1,
    Func<T, B, C> f2)  { ... }
  public Failable<T> Where(Func<T, bool> f) { ... }
}
Failable<int> f = OperationThatCanFail();
Failable<double> d = from i in f where i > 0 select Math.log(i);
public static Func<A, Failable<R>> ToFailable(this Func<A, R> f)
{
  return a => 
  {
    try 
    { 
      return new Failable<R>(f(a));
    }
    catch(Exception x) 
    {
      return new Failable<R>(x);
    }
  };
}

Context

StackExchange Code Review Q#163171, answer score: 31

Revisions (0)

No revisions yet.