Skip to main content

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.

Comments

Popular posts from this blog

async.h - asynchronous, stackless subroutines in C

The async/await idiom is becoming increasingly popular. The first widely used language to include it was C#, and it has now spread into JavaScript and Rust. Now C/C++ programmers don't have to feel left out, because async.h is a header-only library that brings async/await to C! Features: It's 100% portable C. It requires very little state (2 bytes). It's not dependent on an OS. It's a bit simpler to understand than protothreads because the async state is caller-saved rather than callee-saved. #include "async.h" struct async pt; struct timer timer; async example(struct async *pt) { async_begin(pt); while(1) { if(initiate_io()) { timer_start(&timer); await(io_completed() || timer_expired(&timer)); read_data(); } } async_end; } This library is basically a modified version of the idioms found in the Protothreads library by Adam Dunkels, so it's not truly ground bre

Building a Query DSL in C#

I recently built a REST API prototype where one of the endpoints accepted a string representing a filter to apply to a set of results. For instance, for entities with named properties "Foo" and "Bar", a string like "(Foo = 'some string') or (Bar > 99)" would filter out the results where either Bar is less than or equal to 99, or Foo is not "some string". This would translate pretty straightforwardly into a SQL query, but as a masochist I was set on using Google Datastore as the backend, which unfortunately has a limited filtering API : It does not support disjunctions, ie. "OR" clauses. It does not support filtering using inequalities on more than one property. It does not support a not-equal operation. So in this post, I will describe the design which achieves the following goals: A backend-agnostic querying API supporting arbitrary clauses, conjunctions ("AND"), and disjunctions ("OR"). Implemen

Easy Automatic Differentiation in C#

I've recently been researching optimization and automatic differentiation (AD) , and decided to take a crack at distilling its essence in C#. Note that automatic differentiation (AD) is different than numerical differentiation . Math.NET already provides excellent support for numerical differentiation . C# doesn't seem to have many options for automatic differentiation, consisting mainly of an F# library with an interop layer, or paid libraries . Neither of these are suitable for learning how AD works. So here's a simple C# implementation of AD that relies on only two things: C#'s operator overloading, and arrays to represent the derivatives, which I think makes it pretty easy to understand. It's not particularly efficient, but it's simple! See the "Optimizations" section at the end if you want a very efficient specialization of this technique. What is Automatic Differentiation? Simply put, automatic differentiation is a technique for calcu