Lets build a maybe library in C# - Maybe extensions
I thought I would add a couple of extensions to the Maybe monad to show how using monadic patterns can spread through your code when you start to use them and that is a good thing as they carry much power.
If you missed the story so far:
These new operations are extension methods. Not sure this is the correct home at the moment but until I get the repo up and running it will do :)
public static class Maybe
{
public static IMaybe<T> ToSome<T>(this T value)
=> new Some<T>(value);
public static IMaybe<T> ToMaybe<T>(this T value)
=> value == null ? Maybe<T>.None : new Some<T>(value);
public static IMaybe<T> MaybeFirst<T>(this IEnumerable<T> enumerable)
=> enumerable.MaybeFirst(_ => true);
public static IMaybe<T> MaybeFirst<T>(
this IEnumerable<T> enumerable, Func<T, bool> predicate)
=> enumerable.FirstOrDefault(predicate).ToMaybe();
public static IMaybe<TValue> MaybeFind<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary, TKey key)
{
TValue value;
return dictionary.TryGetValue(key, out value)
? value.ToMaybe()
: Maybe<TValue>.None;
}
public static Func<IMaybe<T>, IMaybe<TResult>> Lift<T, TResult>(
Func<T, TResult> function)
=> maybe => maybe.Select(function);
}
So a quick run down
- MaybeFirst - Like IEnumerable FirstOrDefault but returns a None if there is no first element in the collection. Comes in standard and predicate versions to mirror the LINQ operations.
- MaybeFind - You have to admit TryGetValue is utter pish, the need to declare and use an out param, I have thrown up in my mouth thinking about it. This is far cleaner, it returns a Some(value) or None when the key is not present.
- Lift - This one is a little more esoteric. It takes a function and converts it into a function that operates on IMaybe values instead. Handy more moving code into working in the monad space. In a way this is a different version of Select that supports a different use case.
So lets have some fun with these extensions.
var people = // IDictionary<string, Person>
var bobsCity = people
.MaybeFind("Bob")
.SelectMany(bob => bob.HomeAddress)
.Select(address => address.City)
.Recover("Unknown")
.Value
Here we lookup Bob by name in the storage. Bob might have a home address and if so we select the address city and if we fail at any stage we return an "Unknown" address.
Goodbye fugly TryGet semantics and not a single if in the whole block even though the lookup could fail at any stage.
The use of SelectMany is interesting, this is required because the HomeAddress is a Maybe wrapped value so this requires SelectMany to flattern the lookup. Refer back to the type signatures for Select and SelectMany and the Person object and it will be obvious.
Think and how you can use IEnumerable.SelectMany on a collection of collections to flatten it . IMaybe is really a collection that can hold 0 or 1 values so the same principle apples.
As always ask if you have any questions
Happy coding
Woz