This is the eighteenth post in my ongoing series covering the abstractions in Sasa. Previous posts:
- Sasa.Parsing - type-safe, extensible lexing and parsing framework
- Sasa.Dynamics - type-safe polytypic/reflective programming
- Sasa.Func - Type-Safe Delegate Combinators
- Sasa.Option - Handling Optional Values
- Sasa.Result - Handling Exceptional Values
- Sasa.Numbers - Generic Number Extensions
- Sasa.Strings - General String Extensions
- Sasa.Types - Runtime Types And CLR Metadata
- Sasa.Weak - Typed Weak References
- Sasa's Tuples
- Sasa's Core Interfaces
- Sasa.Events - Type-Safe, Null-Safe, Thread-Safe Events
- Sasa.Web.Url64 - URL-Safe Base64 Encoding
- Sasa.Operators<T> - Generic Arithmetic and Logical Operators
- Sasa.IO.FilePath - Easy and Safe Path Manipulations
- Sasa.IO.Streams - Convenient Stream Extensions
- Sasa.Linq.Enumerables - Extensions on IEnumerable<T>
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