Tuesday, May 28, 2013

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.

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