Compilation of different programming
projects I amuse myself with.
Wednesday, 26 June 2024
Functional programming - Guarding against nulls with the Option monad
Monads are "elementary individual substances". We can call them "building blocks" in Functional Programming (FP) and FP is built upon such building blocks to compose more complex logic.
This article sums up using the
Option
monad, which is described many places on the net and is wellknown. This article is just a walkthrough of this monad, which creates a wrapper for a value that will aid against guarding against nulls.
We will effectively abstract away null, and using extension methods we will allow setting up a processing chain where nulls are wrapped inside a value inside the Option objects.
First off, define a generic class for this monad and in this case we will consider reference types or classes (this includes strings).
Also, equality operators can be added to make it easier to do equality checks. The class will be called Option and we will define two methods called None and Some. The field
called _content of type T will be returned inside a Option wrapper instance and None will only return Option object where _content is null.
None() signals that the payload or _content is null. None method returns an instance of Option of T where the wrapped value is null.
Some() signals that the payload or _content> is not null. some method returns an instance of Option of T where the wrapped value is not null.
Both the None() and Some() are of type Option<T> and can be used in a chained expression.
Some concepts of what the Option should support :
A map method that allows transforming from T to TResult (both can be same type) along the chained expressions
A reduce method that allows retrieving the value inside the Option object that is wrapped
Extension methods for going from an object to an Option wrapper of the object
Predicate based filter methods for peeling away Option objects where the wrapped object which satisfy the filter can be filtered
publicclassOption<T> : IEquatable<Option<T>> whereT : class {
private T? _content;
privateOption(){}
publicstatic Option<T> None() => new();
publicstatic Option<T> Some(T obj) => new Option<T> { _content = obj };
publicoverrideboolEquals(object? obj)
{
returnthis.Equals(obj as Option<T>);
}
public Option<T> Where(Func<T, bool> predicate) =>
_content isnotnull && predicate(_content) ? this : Option<T>.None();
public Option<T> WhereNot(Func<T, bool> predicate) =>
_content isnotnull && !predicate(_content) ? this : Option<T>.None();
publicboolEquals(Option<T>? other) => other isnull ? false :
_content?.Equals(other._content) ?? false;
publicstaticbooloperator ==(Option<T>? a, Option<T>? b) =>
a isnull ? b isnull : a.Equals(b);
publicstaticbooloperator !=(Option<T>? a, Option<T>? b) => !(a == b);
publicoverrideintGetHashCode()
{
return _content?.GetHashCode() ?? 0;
}
publicOption<TResult> MapOptional<TResult>(Func<T, Option<TResult>> map) where TResult : class =>
_content isnotnull ? map(_content) : Option<TResult>.None();
publicOption<TResult> Map<TResult>(Func<T, TResult> map) where TResult : class =>
new Option<TResult>() { _content = _content isnotnull ? map(_content) : null };
public T Reduce(T orElse) => _content ?? orElse;
public T Reduce(Func<T> orElse) => _content ?? orElse();
}
We also add some extension methods to help using the monad Option above containing the Some and None methods :
publicstaticclassOptionExtensions {
publicstaticOption<T> ToOption<T>(this T? obj) where T : class => obj isnotnull ? Option<T>.Some(obj) : Option<T>.None();
publicstaticOption<T> Where<T>(this T? obj, Func<T, bool> predicate) where T : class => obj isnotnull && predicate(obj) ? Option<T>.Some(obj) : Option<T>.None();
publicstaticOption<T> WhereNot<T>(this T? obj, Func<T, bool> predicate) where T : class => obj isnotnull && !predicate(obj) ? Option<T>.Some(obj) : Option<T>.None();
}
Let's also add a ForEach extension method for easier usage in chained expressions of ienumerable collections.
The demo code then looks like below, where we iterate through an array of names and grab the initials of the name with some simple logic. Note usage of WhereNot predicate to avoid having to think about null strings further down the chain and also the usage of the Reduce method to get the value which the Option monad wraps.
The example in this article with retrieving initial (first letter) of some names is trivial. The real benefit of using the Option is to safeguard against nulls in lengthy chained calls or other scenarios where you want to always return something and decide how to indicate that we got something back and decide what an empty object will be, in the example the letter "?" signals a null based name.
No comments:
Post a Comment