Skip to main content

Sasa.Either - Simple Sums for .NET

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

Tuples are a pretty common necessity while programming, and every programmer has run into the need to return multiple values from a method. The algebraic description of a tuple is what's known as a "product", and the logical analogue is the "conjunction", ie. a product/tuple is type T0 AND T1 AND T2 AND ...

Given any abstraction, it's dual will often also be necessary. Imagine programming only with the logical AND operator without OR! Analogously, we also need the algebraic analogue of products which are known as "sums", ie. type T0 OR T1 OR T2 OR ...:

Either<int, string> foo = "hello world!";
Console.WriteLine(foo);
// output:
// hello world!

The need for this may not be so obvious to CLR programmers because inheritance provides a kind of sum. For instance, the above code snippet could be modelled with an explicit class hierarchy:

abstract class IntOrString
{
    public static implicit operator IntOrString(int i)
    {
        return new IntCase { value = i };
    }
    public static implicit operator IntOrString(string s)
    {
        return new StringCase { value = s };
    }
}
sealed class IntCase : IntOrString
{
    int value;
    public override string ToString() { return value.ToString(); }
}
sealed class StringCase : IntOrString
{
    string value;
    public override string ToString() { return value; }
}
...
IntOrString foo = "hello world!";
Console.WriteLine(foo);
// output:
// hello world!

However, just like with tuples, when you're only dealing with a small number of possibilities you don't want to go through the hassle of creating a whole new set of types.

Alternately, you can just use the implicit supertype of all CLR types, System.Object instead of creating a class hierarchy, but in a very real sense this is TOO permissive, since it allows more than just System.Int32 and System.String:

object foo = new Action(() => throw new Exception("Fooled you!"));
Console.WriteLine(foo);
// output:
// System.Action ...

Sasa.Either<T0,T1>, Sasa.Either<T0,T1,T2> and Sasa.Either<T0,T1,T2,T3> are generic sum types that provide this functionality for you, along with a few convenient operations to make programming with sums easier. For instance, ToString is overloaded to return the string representation of the underlying value, there are implicit conversions into the corresponding sum type, equality is structural over the various cases, and value extraction from sums is done via a simple and familiar deconstruction scheme:

Either<int, string> foo = "hello world!";
int icase;
if (foo.TryFirst(out icase))
{
    Console.WriteLine("int: " + icase);
    return;
}
string scase;
if (foo.TrySecond(out scase))
{
    Console.WriteLine("string: " + scase);
    return;
}
// output:
// string: hello world!

Sasa.Either.Coalesce

The ?? null coalescing operator only works on values of the same type, where sums can work on values of disparate types. Thus Either.Coalesce is a set of overloads that null coalesce the parameters and returns an appropriate sum encapsulating the first non-null value:

var sum = Either.Coalesce<string, float>(null, 3.0F);
Console.WriteLine(sum);
// output:
// 3.0

Sasa.Either.First

Either.First is a simple set of sum constructors for the first case of a sum:

var x = Either.First<int, string, decimal>(3);
Console.WriteLine(x);
// output:
// 3

Sasa.Either.Second

Either.Second is a simple set of sum constructors for the second case of a sum:

var x = Either.Second<int, string, decimal>("hello!");
Console.WriteLine(x);
// output:
// hello!

Sasa.Either.Third

Either.Third is a simple set of sum constructors for the third case of a sum:

var x = Either.Third<int, string, decimal>(99M);
Console.WriteLine(x);
// output:
// 99.0

Sasa.Either.Fourth

Either.Fourth is a simple set of sum constructors for the fourth case of a sum:

var x = Either.Fourth<int, string, float, DateTime>(DateTime.MinValue);
Console.WriteLine(x);
// output:
// DateTime.MinValue

Sasa.Either<*>.First

Sasa.Either<*>.First (2-param, 3-param, 4-param) attempts to extract the encapsulated value of type T0. An Option<T> is returned indicating success or failure, which means you can use all the usual coalescing operators on options with the results:

Either<int, string> foo = "hello world!";
Console.WriteLine(foo.First || 123);
// output:
// 123

Sasa.Either<*>.Second

Sasa.Either<*>.Second (2-param, 3-param, 4-param) attempts to extract the encapsulated value of type T1. An Option<T> is returned indicating success or failure, which means you can use all the usual coalescing operators on options with the results:

Either<int, string> foo = "hello world!";
Console.WriteLine(foo.Second || "Impossible!");
// output:
// hello world!

Sasa.Either<*>.Third

Sasa.Either<*>.Third (3-param, 4-param) attempts to extract the encapsulated value of type T2. An Option<T> is returned indicating success or failure, which means you can use all the usual coalescing operators on options with the results:

Either<int, string, decimal> foo = "hello world!";
Console.WriteLine(foo.Third || 99M);
// output:
// 99.0

Sasa.Either<T0, T1, T2, T3>.Fourth

Sasa.Either<T0, T1, T2, T3>.Fourth attempts to extract the encapsulated value of type T3. An Option<T> is returned indicating success or failure, which means you can use all the usual coalescing operators on options with the results:

Either<int, string, decimal, DateTime> foo = "hello world!";
Console.WriteLine(foo.Third || DateTime.MinValue);
// output:
// DateTime.MinValue

Sasa.Either<*>.IsFirst

Sasa.Either<*>.IsFirst (2-param, 3-param, 4-param) checks whether the sum is the first case of type T0:

Either<int, string, decimal, DateTime> x = 3;
Console.WriteLine(x.IsFirst);
Console.WriteLine(x.IsSecond);
// output:
// true
// false

Sasa.Either<*>.IsSecond

Sasa.Either<*>.IsSecond (2-param, 3-param, 4-param) checks whether the sum is the first case of type T1:

Either<int, string, decimal, DateTime> x = "hello!";
Console.WriteLine(x.IsFirst);
Console.WriteLine(x.IsSecond);
// output:
// false
// true

Sasa.Either<*>.IsThird

Sasa.Either<*>.IsThird (3-param, 4-param) checks whether the sum is the first case of type T2:

Either<int, string, decimal, DateTime> x = 99M;
Console.WriteLine(x.IsFirst);
Console.WriteLine(x.IsThird);
// output:
// false
// true

Sasa.Either<T0, T1, T2, T3>.IsFourth

Sasa.Either<T0,T1,T2,T3>.IsFourth checks whether the sum is the first case of type T3:

Either<int, string, decimal, DateTime> x = DateTime.Now;
Console.WriteLine(x.IsFirst);
Console.WriteLine(x.IsFourth);
// output:
// false
// true

Sasa.Either<*>.TryFirst

Sasa.Either<*>.TryFirst (2-param, 3-param, 4-param) method extracts the encapsulated value if the sum is of type T0:

Either<int, string> foo = "hello world!";
int icase;
if (foo.TryFirst(out icase))
{
    Console.WriteLine("int: " + icase);
    return;
}
string scase;
if (foo.TrySecond(out scase))
{
    Console.WriteLine("string: " + scase);
    return;
}
// output:
// string: hello world!

Sasa.Either<*>.TrySecond

Sasa.Either<*>.TrySecond (2-param, 3-param, 4-param) method extracts the encapsulated value if the sum is of type T1:

Either<int, string> foo = "hello world!";
int icase;
if (foo.TryFirst(out icase))
{
    Console.WriteLine("int: " + icase);
    return;
}
string scase;
if (foo.TrySecond(out scase))
{
    Console.WriteLine("string: " + scase);
    return;
}
// output:
// string: hello world!

Sasa.Either<*>.TryThird

Sasa.Either<*>.TrySecond (3-param, 4-param) method extracts the encapsulated value if the sum is of type T2:

Either<int, string, decimal> foo = "hello world!";
int icase;
if (foo.TryFirst(out icase))
{
    Console.WriteLine("int: " + icase);
    return;
}
string scase;
if (foo.TrySecond(out scase))
{
    Console.WriteLine("string: " + scase);
    return;
}
string dcase;
if (foo.TryThird(out dcase))
{
    Console.WriteLine("decimal: " + dcase);
    return;
}
// output:
// string: hello world!

Sasa.Either<T0,T1,T2,T3>.TryFourth

Sasa.Either<*>.TryFourth method extracts the encapsulated value if the sum is of type T3:

Either<int, string, decimal, DateTime> foo = "hello world!";
int icase;
if (foo.TryFirst(out icase))
{
    Console.WriteLine("int: " + icase);
    return;
}
string scase;
if (foo.TrySecond(out scase))
{
    Console.WriteLine("string: " + scase);
    return;
}
string dcase;
if (foo.TryThird(out dcase))
{
    Console.WriteLine("decimal: " + dcase);
    return;
}
string tcase;
if (foo.TryFourth(out tcase))
{
    Console.WriteLine("DateTime: " + tcase);
    return;
}
// output:
// string: hello world!

As you can see from the above code samples, there are also implicit conversions for easy construction of sums given a value. Furthermore, equality is overridden and defined on the encapsulated values.

Comments

kbens0n said…
First, I'm a complete NOOB to C#. Am suspecting that sample code under Sasa.Either.Third has inadvertent typo (it reads = Either.Second)? Additionally, was wondering if the // output of above sample AND code sample for Sasa.Either<*>.Third should be exactly the same? And should that be 99.0 or 99M (or 99.00)? Thanks for this fantastic volume of work on your part !
Sandro Magi said…
Thanks for the corrections! You've got it exactly right.

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

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