The function Map()
Today I would like to share last of small helper functions that help write concise code. Just like Apply, it helps to avoid null-checking ifs
by conditionally applying a transformation function. Differently from Apply, it returns the result of the transformation, which, apparently, is quite a frequently used pattern.
Let's check this code fragment:
var user = _users.FindByName(name);
if (user != null)
{
var contract = _contractBuilder.Create(user);
if (contract != null)
{
var result = _sender.Send();
if (result != null)
Console.WriteLine(result.ToString());
}
}
There are many ways in which this code could be rewritten.
For example, you may prefer removing nesting:
var user = _users.FindByName(name);
if (user == null) return;
var contract = _contractBuilder.Create(user);
if (contract == null) return;
var result = _sender.Send();
if (result == null) return;
Console.WriteLine(result.ToString());
However, it still has quite a lot of null checks.
Would it better to write something like this instead?
_users.FundByName(name)
.Map(user => _contractBuilder.Create(user))
.Map(contract => _sender.Send())
.Apply(result => Console.WriteLine(result));
You probably already know what Apply does, but what is Map? It is actually quite simple:
static V Map<T, V>(this T value, Func<T, V> func) =>
EqualityComparer<T>.Default.Equals(value, default) ? default : func(value);
It could be the end, but there is a bit more behind it. If you like the functional programming, you might have recognised the pattern. It is a monad, but a bit unusual one.
According to this quite general tutorial and this a bit more specific C# tutorial, monad is essentially a container type with a function that conditionally transforms the content of the container AND returns the value wrapped in the same container type.
Probably the most common example of this concept is Maybe (Optional) generic type:
// general monad pattern
class Monad<T> {
Monad(T instance);
Monad<U> Bind(Func<T, Monad<U>> func);
}
// Maybe monad
class Maybe<T> {
private T _value;
Maybe(T value = null) => _value = value;
Maybe<U> Bind(Func<T, Monad<U>> func) =>
_value == null ? new Maybe() : func(_value)
}
It transforms the example from the beginning of this article in this way:
new Maybe(_users.FundByName(name))
.Bind(user => new Maybe(_contractBuilder.Create(user)))
.Bind(contract => new Maybe(_sender.Send()))
.Bind(result => {
Console.WriteLine(result);
return new Maybe(System.Unit);
});
In order to understand, why Map
is actually a part of a monadic pattern, we need to look closely on the characteristics of the type inside the Map
's func
argument, to what Map
applies it, and what it returns.
As Map
is an extension method, it can be "called" with practically anything. But let's narrow down this list to reference types. This gives us the following:
Map
can applied to any variable, in fact. The variable itself is a container. This container stores a reference ornull
.- Inside the function, its argument is guaranteed to not be
null
. - And the function returns either a reference or
null
.
Using C# null reference type annotations:
static V? Map<T, V>(this T? value, Func<T, V?> func);
It might be clear already, but let's write it down in another form:
variable_that_can_be_null
.Map(variable_that_cannot_be_null => {
... some code that returns variable_that_can_be_null
})
Language purist may reasonably argue here that it is not a C# monad, because an extension method isn't in fact a method or variable in the Map
's function is in fact of the same type. It is all true, of course. But for me if it walks like a duck, quacks like a duck, and adheres to Monad Laws, it is a monad.
I hope it was useful to you and If it really was, please subscribe to updates, share this article, or upvote on reddit. It is really important to me to know that you like it and it makes a huge difference. All the best!