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

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

Easy Reverse Mode Automatic Differentiation in C#

Continuing from my last post on implementing forward-mode automatic differentiation (AD) using C# operator overloading , this is just a quick follow-up showing how easy reverse mode is to achieve, and why it's important. Why Reverse Mode Automatic Differentiation? As explained in the last post, the vector representation of forward-mode AD can compute the derivatives of all parameter simultaneously, but it does so with considerable space cost: each operation creates a vector computing the derivative of each parameter. So N parameters with M operations would allocation O(N*M) space. It turns out, this is unnecessary! Reverse mode AD allocates only O(N+M) space to compute the derivatives of N parameters across M operations. In general, forward mode AD is best suited to differentiating functions of type: R → R N That is, functions of 1 parameter that compute multiple outputs. Reverse mode AD is suited to the dual scenario: R N → R That is, functions of many parameters t...