27
\$\begingroup\$

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<bool, T>, 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<T> 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<string> value = null; is invalid, but Failable<string> value = (Failable<string>)null; is, as well as Failable<string> value = new Failable<string>(null);.

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

public struct Failable<T> : IEquatable<Failable<T>>
{
    public Exception Exception { get; }
    public T Result { get; }
    public bool Passed { get; }

    private Failable(Exception exception, T result, bool passed)
    {
        Exception = exception;
        Result = result;
        Passed = passed;
    }

    public Failable(Exception exception)
        : this(exception, default(T), false)
    {
    }

    public Failable(T result)
        : this(null, result, true)
    {
    }

    public static implicit operator Failable<T>(Exception exception) => new Failable<T>(exception);
    public static implicit operator Failable<T>(T result) => new Failable<T>(result);

    public static implicit operator Exception(Failable<T> result) => result.Exception;
    public static implicit operator T(Failable<T> result) => result.Result;

    public override string ToString() => (Passed ? Result?.ToString() : Exception?.ToString()) ?? "null";
    public override int GetHashCode() => Exception.GetHashCode() ^ Result.GetHashCode();

    public bool Equals(Failable<T> other) => this == other;
    public override bool Equals(object obj) => obj is Failable<T> && this == (Failable<T>)obj;
    public static bool operator ==(Failable<T> a, Failable<T> b) => a.Exception == b.Exception && a.Result.Equals(b.Result);
    public static bool operator !=(Failable<T> a, Failable<T> b) => a.Exception != b.Exception || !a.Result.Equals(b.Result);

    public static readonly Failable<T> Empty = new Failable<T>();
}

Now, to demonstrate how this works I defined a very ugly method, so do not review it please, that goes through all the possible features of this struct:

static Failable<T> FailableTest<T>(bool pass, bool nullOrThrow, T result)
{
    try
    {
        if (pass)
        {
            if (nullOrThrow)
            {
                // Both options are valid:
                // return new Failable<T>(null);
                return (Failable<T>)null;
            }
            else
            {
                return result;
            }
        }
        else
        {
            if (nullOrThrow)
            {
                throw new ArgumentException($"Throwing as expected, {nameof(pass)}:'{pass}', {nameof(nullOrThrow)}:'{nullOrThrow}'.");
            }
            else
            {
                return new ArgumentException($"Returning as expected, {nameof(pass)}:'{pass}', {nameof(nullOrThrow)}:'{nullOrThrow}'.");
            }
        }
    }
    catch (Exception e)
    {
        return e;
    }
}

Our test cases are something on the order of:

Console.WriteLine("Pass  : " + FailableTest(true, false, "1. String on pass").ToString());
Console.WriteLine("Fail  : " + FailableTest(false, false, "2. String on pass").ToString());
Console.WriteLine("Null  : " + FailableTest(true, true, "3. String on pass").ToString());
Console.WriteLine("Throw : " + FailableTest(false, true, "4. String on pass").ToString());
Console.WriteLine("Cast  : " + (FailableTest(true, false, 15) - FailableTest(true, false, 5)));

Returning:

Pass  : 1. String on pass
Fail  : System.ArgumentException: Returning as expected, pass:'False', nullOrThrow:'False'.
Null  : null
Throw : System.ArgumentException: Throwing as expected, pass:'False', nullOrThrow:'True'.
   at GenericFailableTest.Program.FailableTest[T](Boolean pass, Boolean nullOrThrow, T result) in c:\users\ebrown\documents\visual studio 2017\Projects\Test CSharp Projects\GenericFailableTest\Program.cs:line 44
Cast  : 10

The interesting thing about this is that the implicit operator T operator allows you to just ignore this class altogether:

var str = FailableTest(true, false, "Some String");

And this is by design, which brings me to my main list of questions:

  1. Should there be a T(Failable<T>) operator? If so, should it be implicit?
  2. Should there be an Exception(Failable<T>) operator? If so, should it be implicit?
  3. Should the API include a Failable(Tuple<bool, T>) constructor that allows one to pass a tuple of (pass, value)? A proper definition might be:
static Failable<int> TryParse(string input)
{
    int result;

    if (int.TryParse(input, out result))
    {
        return result;
    }
    else
    {
        return new ArgumentException($"The string '{input}' was not a valid integer.");
    }
}

var parseResult = new Failable<int>();
while (!parseResult.Passed)
{
    parseResult = TryParse(Console.ReadLine());
}

var value = parseResult.Result;
// Do something with `value`

1: The term 'good' here is subjective, there are two major alternatives to throwing exceptions already present in the language:

1. Use out parameters;
2. Return a Tuple;

Either of these are 'good' in certain lights; I'm simply attempting to draw out another possible alternative.

\$\endgroup\$
  • 6
    \$\begingroup\$ Reinventing the wheel? See this Refactoring away from exceptions I can tell you, this is a horrible idea. I tried to use and and the result was a total mess so I undone everything and now use the classic way of exceptions. It's much easier. Once you start using a result/failable you'll be in a vicious circle of results/failables with insane ifs everywhere. Your implementation is just a simple beginning. \$\endgroup\$ – t3chb0t May 12 '17 at 15:09
  • 1
    \$\begingroup\$ @t3chb0t I think it is okay to use this class in the same way int.TryParse method is used in above example, where you check the Passed state instantly, as soon as method returns and work only with Result from there. But once you start passing those Failable-s up the stack (as normal exception would go) I can see it becoming a problem. \$\endgroup\$ – Nikita B May 12 '17 at 15:30
  • 1
    \$\begingroup\$ @NikitaB exactly, and it's even more interesing when you have an IEnumerable of failables/results and only some of them failed and the others not or when the T itself is an IEnumerable. \$\endgroup\$ – t3chb0t May 12 '17 at 15:39
  • 5
    \$\begingroup\$ I'm curious as to why you guys think this is a bad idea; Rust has Result<T, TErr> and does just fine. Granted, it's used everywhere in the std lib so it has much more usage than this would in C#. \$\endgroup\$ – Dan Pantry May 12 '17 at 17:03
  • 2
    \$\begingroup\$ Ok, then the API isn't consistant because at one time the Exception can be accessed via the Exception property and another time it's the Result. In this case there's no way to know whether the Exception as a Result is the actual result of a fuction call or an indicator of an error. What if I have a function that actually creates an Exception, some ExceptionFactory, and it failed to create an Exception, now the result contains an Exception but is the Exception the one the factory should have created or is it an error? \$\endgroup\$ – t3chb0t May 12 '17 at 18:40
31
\$\begingroup\$

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<T>. 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<T>) 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<T>) operator? If so, should it be implicit?

I would find this confusing.

Should the API include a Failable(Tuple<bool, T>) 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<T> 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<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) { ... }
}

If you do that, then you can write queries:

Failable<int> f = OperationThatCanFail();
Failable<double> 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<T> 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<S, T> 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<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);
    }
  };
}

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

\$\endgroup\$
  • \$\begingroup\$ I haven't heard of a tracing monad (and searching for it doesn't seem to yield anything relevant). What exactly is it tracing, and when would it trace? \$\endgroup\$ – Rob May 12 '17 at 23:58
  • 3
    \$\begingroup\$ @Rob: hackage.haskell.org/package/monad-logger -- you can trace whatever you want. eg, a string arrives on your web server. Where does it go and how is it modified between its arrival and it being (1) used in a query (2) written to a database, (3) echoed to the user? These sorts of data flow questions are good to be able to answer when you think you might have a sql injection, database corruption, cross site scripting, or other attack on your hands. \$\endgroup\$ – Eric Lippert May 13 '17 at 0:01
  • \$\begingroup\$ If this was such a great idea then I'm pretty sure you would already have implemented it in some of the .Net versions but I guess you know that this cannot be done and these excercises should prove that without you saying explicitly don't do it, it cannot be done or C# has other (or better) ways to handle exceptions etc. \$\endgroup\$ – t3chb0t May 13 '17 at 6:05
  • 6
    \$\begingroup\$ @t3chb0t: In C# exceptions should be handled by handling exceptions, which is why we called it "exception handling". The problem arises when you realize that exception handling in C# presupposes that method activations logically form a stack, which is not true for coroutines. C# now has two kinds of coroutines: iterator blocks and async methods. In both cases the compiler team had to come up with all kinds of crazy rules to keep exception handling working. And one of the techniques was using Task<T> as an error monad. It's not clear to me why you think this can't be done; we did it. \$\endgroup\$ – Eric Lippert May 13 '17 at 6:45
  • 2
    \$\begingroup\$ Task works in a different way then the Failable/Result is supposed to so they are not the same idea. In fact the usage of Task is incosistant and as far as consistency is concerned the rules about Failable/Result are clear and easier to apply. A method which returns a Task can either throw or return Task.IsFaulted == true so you can never be sure about the result on the other hand you can always be sure that a Failable/Result/etc method will never throw. That's why I think you didn't do it, you still use exceptions even though Task offers everything to avoid throwing. \$\endgroup\$ – t3chb0t May 13 '17 at 7:14
8
\$\begingroup\$

A few minor things:

  1. You do not check for null in GetHashCode() and in equality operators. You probably should, since both Exception and Result can be null by your design.
  2. You should re-use single equality implementation. For example, != should just return !(a == b).

Otherwise this implementation looks alright to me. I would probably make T conversion implicit and Exception conversion explicit (or just remove it, since you might as well just write .Exception), but that's just me not being a fan of "transparent" things. :)

P.S. Also Failable does not feel like a typical struct to me, more like a class.

\$\endgroup\$
  • 4
    \$\begingroup\$ I want it to be a struct only because I want Failable<T> value = null; to fail, since Failable<T> should always have a state. \$\endgroup\$ – 410_Gone May 12 '17 at 15:36
7
\$\begingroup\$

For the sake of a consistent and discoverable API you should have a non-generic version of Failable.

For Consistency:
If I'm presented with two methods that can fail to do their job, then I'd expect to check for the success/failure in the same way. I shouldn't check result.Passed some times and result == null at other times.

For Discoverability
Using Failable as a return type clues in people that see the code that an operation is might not succeed. Using Exception as a return type misleads people that see the code that the operation has something to do with Exceptions. Given this interface with three methods that might not succeed:

public interface IYouDontKnowMyImplementation
{
    Exception AWellNamedMethod();
    Failable<T> AnotherWellNamedMethod<T>();
    Failable NamingIsHard()
}

Even if I was unfamiliar with your Failable class, at a glance I have a good chance of correctly determining what the return types of AnotherWellNamedMethod and NamingIsHard mean, but would have no reason to assume that AWellNamedMethod returns Exception because it might fail, and wouldn't expect that it returning null indicated it was successful. Which of these is easier to read and understand what is going on?

...
if(someObject.MoveFilesToArchive() == null)
{
    ...
}
if(someObject.MoveFilesToArchive().Passed)
{
    ...
}

What happens when T is Exception?

When the thing your method is supposed to do is build an exception, Your implicit conversions become ambiguous.

Failable<Exception> BuildErrorInformation()
{
    ... Do information gathering
    if(iWasAbleToGetTheInfoINeed)
    {
        return new Exception("the info I need");
    }
    else
    {
        return new Exception("I failed to get the info I needed");
    }
}

One of these code paths should produce a Failable<Exception> where

result.Exception.Message == "I failed to get the info I needed"

and one where

result.Result.Message == "the info I need"

This is really an edge case that can probably be resolved by using Failable<CustomException> instead, or just returning new Failable<Exception>(null, new Exception("the info I need"), true);

Other Comments

These might be borderline nitpicky but still earnest feedback:

  • The order of parameters in function calls often carries an implied weight or importance of the parameters. As such, the choice to have the exception be the first parameter feels very odd to me.
  • Consider naming the sentinel property Success or Complete instead of Passed. Almost all of the status/result/outcome codes that come to mind for conceptually similar operations use 'success' to indicate the 'good' outcome. The one place I've seen 'Passed' used is for validation of data, and even in this scenario, a 'pass' would indicate the data was good, not that the validation completed.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.