Tuesday, March 19, 2013

Sasa.Result - Handling Exceptional Values

This is the fifth post in my ongoing series covering the abstractions in Sasa. Previous posts:

Sasa.Result<T> encapsulating the results of a computation, either it's return value, or the error it generated, if any. For instance, you could spawn a thread to perform some calculation and deposit the result into an instance of Result<T>. Sasa.Result<T> is available in the core Sasa.dll.

Result<T> implements all the usual equality tests on itself, and on T, so you can perform direct comparisons to values.

Sasa.Result<T>.Value

The Sasa.Result<T>.Value property allows clients to obtain the value that was returned from the computation. If an error was instead generated, then this throws InvalidOperationException with the InnerException property set to the original exception:

Result<int> hasValue = 3;
Result<int> noValue = new Result<int>(new ArgumentException("somearg"));
Console.WriteLine(hasValue.Value);
Console.WriteLine(noValue.Value); //InvalidOperationException
// outputs:
// 3
// InvalidOperationException

This property is also required by IValue<T>, which Result<T> implements.

Sasa.Result<T>.HasValue

The Sasa.Result<T>.HasValue property allows clients to check whether the result is a legitimate value, or if it's an error result. If HasValue returns true, then clients can safely access the Value property. If it's false, then doing so will throw InvalidOperationException:

Result<int> hasValue = 3;
Result<int> noValue = new Result<int>(new ArgumentException("somearg"));
Console.WriteLine(hasValue.HasValue);
Console.WriteLine(noValue.HasValue);
// outputs:
// true
// false

This property is also required by the IOptional<T>, which Option<T> implements.

Sasa.Result<T>.Error

The Sasa.Result<T>.Error property allows clients to access the exception that resulted from the computation, with the full stack trace intact:

void EnsureValue<T>(Result<T> result)
{
  if (!result.HasValue)
      Environment.FailFast("Unexpected result!", result.Error);
}

This property is also required by IResult<T>, which Result<T> implements.

Sasa.Result<T>.TryGetValue

TryGetValue permits accessing the encapsulated Value without throwing an exception if the result was an error:

Result<int> someInt = 3;
int x;
if (someInt.TryGetValue(out x))
  Console.WriteLine(x);
else
  Console.WriteLine(someInt.Error);
 
Result<int> noInt = new ArgumentException("noInt");
if (noInt.TryGetValue(out x))
  Console.WriteLine(x);
else
  Console.WriteLine(noInt.Error);
// output is:
// 3
// System.ArgumentException: ...

This method is required by the IVolatile<T> interface, which Result<T> implements.

Sasa.Result<T>.| operator

The C# null coalescing operator, ??, is hard-coded to only apply to reference types or System.Nullable<T>. However, the short-circuiting logical-or operator can be overridden, and Result<T> does so to provide result-coalescing:

Result<int> someInt = 3;
Result<int> noInt = new ArgumentException("noInt");
Console.WriteLine(someInt || 99);
Console.WriteLine(noInt || 99);
// output is:
// 3
// 99

Sasa.Result<T>.Select

The Sasa.Result<T>.Select is the first in the series of methods used in the LINQ query pattern:

Result<int> someInt = 3;
Result<int> noInt = new ArgumentException("noInt");
Console.WriteLine(someInt.Select(x => x > 0));
Console.WriteLine(noInt.Select(x => x > 0));
// output is:
// true
// Result<ArgumentException>

There is also an implementation of Select for the more abstract IResult<T> interface, but because Result<T> is a struct this would incur too much unnecessary boxing, so we override the Select and SelectMany extension methods with instance methods.

Sasa.Result<T>.Where

Sasa.Result<T>.Where method implements the LINQ where clause for query patterns on Result<T>:

Result<int> someInt = 3;
Result<int> noInt = Result.Fail<int>(new NotSupportedException("noInt"));
Console.WriteLine(someInt.Where(x => x > 0));
Console.WriteLine(someInt.Where(x => x < 0));
Console.WriteLine(noInt.Where(x => x == 0));
// outputs:
// true
// Result<ArgumentException>
// Result<NotSupportedException>

Sasa.Result<T>.SelectMany

The most essential LINQ operator, the Sasa.Result<T>.SelectMany overloads implement chaining for result values:

var val = from y in 2.ToResult()
          from j in 7.ToResult()
          from x in (y + 2 + j).ToResult()
          select x;
Console.WriteLine(val);
 
var noval = from y in 2.ToResult()
            from j in Result.Fail<int>(new ArgumentException("j"))
            from x in (y + 2 + j).ToResult()
            select x;
Console.WriteLine(noval);
// output:
// 11
// Result<ArgumentException>

There is also an implementation of SelectMany for the more abstract IResult<T> interface, but because Result<T> is a struct this would incur too much unnecessary boxing, so we override the Select and SelectMany extension methods with instance methods.

Sasa.Result.Try

The Sasa.Result.Try static extension methods allow clients to execute some code within an exception handling block, which then returns the appropriate result for you, ie. either a value or an error:

Result<int> someInt = Result.Try(() => 3);
Result<int> noInt = Result.Try<int>(() => throw new Exception());
Console.WriteLine(someInt);
Console.WriteLine(noInt);
// output is:
// 3
// Result<Exception>

Sasa.Result.Fail

The static Sasa.Result.Fail method creates a result failure of the appropriate type with the given exception:

Result<int> fail = Result.Fail<int>(new ArgumentException("fail"));
Console.WriteLine(fail);
// outputs:
// Result<ArgumentException>

Sasa.Result.Success

The static Sasa.Result.Success method creates a successful result value of the given type:

Result<int> x = Result.Success(99);
Console.WriteLine(x);
// outputs:
// 99

Sasa.Result.ToResult

The Sasa.Result.ToResult extension method overloads provide some simple conversions from values into results:

Result<int> someInt = 3.ToResult();
Result<int> optInt = 3.ToOption().ToResult();
Result<IEnumerable<int>> intList = new int[] { 3, 99, 66 }.ToResult();

The last overload over IEnumerable is useful because such sequences are often lazily evaluated, which means they may have hidden errors that you will only incur while iterating. This overload then turns a lazy sequence of possible unsafe errors into a lazy sequence of exception-safe result types.

No comments: