With functional programming on the rise nowadays,
more and more people start using functional
patterns in their code.
One of the simplest patterns is Maybe<T>
monad
also called Option<T>
or Optional<T>
.
Maybe<T>
’s primary use case is to represent a possibly
missing value.
I have already use Maybe<T>
a few times in real
codebases and in this post I want to gather my thoughts
on Maybe<T>
and how, I think, it should be used.
Why we use Maybe<T>
?
Most people that use Maybe<T>
generally fall into
one of the two categories.
Category 1: Wants to eliminate NullReferenceException
For a long time before Maybe<T>
programmers tried to
clearly state to the clients of their API that a given
method may return null
instead of an object.
Some of them used special naming conventions or
comments, for example:
// Naming convention and XML documentation
// comments in action.
public interface IUserRepository {
/// <returns>
/// Returns <c>null</c> if user is not found.
/// </returns>
User FindById(UserId id);
/// <exception cref="EntityNotFound">
/// If user is not found.
/// </exception>
User FindRequiredById(UserId id);
}
Others resorted to using special marking attributes and static code analysis tools. A good example of this category is JetBrains.Annotations package, that can be used together with ReSharper to detect missing null checks:
public interface IUserRepository {
[CanBeNull]
User FindById(UserId id);
}
public interface IUserService {
void ActivateUser([NotNull] User user);
}
Yet another solution to this problem were Code Contracts developed by Microsoft.
None of those solutions is perfect and Maybe<T>
seems
to offer a better alternative.
Why? Because it is checked by the compiler,
does not require additional tools and does not slow
down compilation.
But remember there is
no silver bullet,
and Maybe<T>
is not perfect either.
We can use Maybe<T>
like this:
public interface IUserRepository {
Maybe<User> FindById(UserId id);
}
// and usage:
var user = usersRepository.FindById(userId);
if (user.HasValue) {
return Ok(user);
}
else {
return NotFound();
}
If you find yourself in this category of programmers, you would definitely want to use a lightweight library that does not force you to embrace a more functional style. One good library that I can recommend is: CSharpFunctionalExtensions.
If you choose a different library, please make sure to check
that Maybe<T>
is implemented using struct
, otherwise you
may be surprised:
Maybe<string> GetUserAgent() {
return null;
}
One small downside of using a struct
is
possibility of declaration of a nullable Maybe<T>
type:
// Don't do this:
Maybe<string>? bad;
When I was writing this article I tried to gather some
best practices of using Maybe<T>
.
Unfortunately it was very difficult to provide a comprehensive
list. There is not much material on this on the web (I mean using
Maybe<T>
not in the FP fashion) and the available
material is often contradictory.
And so instead of providing you with a list of best practices,
I will only give you some hints where you can look for advice.
We should start our search by looking at
Java 8, which was published in 2014 and introduced
java.util.Optional<T>
class.
The purpose of this class is to be a nullability marker
for method results just like our Maybe<T>
.
There are a lot of articles about how Optional<T>
should
and should not be used.
A good starting point will be
this SO question
with the first two answers.
From this question alone we may learn e.g. to never wrap
a collection into Maybe<T>
,
instead of we should return a possibly empty collection.
The downside of reading Java’s best practices is that some of them do not apply to C#. For example let’s look at the advice given in this SO answer:
When a method can accept optional parameters, it’s preferable to adopt the well-proven approach and design such case using method overloading.
In other words author suggest to change:
// this ctor:
public SystemMessage(
string title,
string body,
Maybe<Attachment> attachment);
// into two ctor's:
public SystemMessage(
string title,
string body,
Attachment attachment);
public SystemMessage(
string title,
string body);
// Because using these ctor's would be easier for
// the clients. Consider:
new SystemMessage("foo", "bar", Maybe<Attachment>.None)
new SystemMessage("foo", "bar",
Maybe<Attachment>.From(attachment))
// vs
new SystemMessage("foo", "bar");
new SystemMessage("foo", "bar", attachment);
But this argument does not applies to C#, where we can use implicit conversion operator with default parameters to achieve exactly the same effect without using overloads:
public struct Maybe<T> {
private T _value;
private bool _present;
public Maybe(T value) {
_value = value;
_present = (value != null);
}
public static implicit operator Maybe<T>(T value)
=> value == null ? new Maybe<T>() : new Maybe<T>(value);
}
public class Attachment { }
public class SystemMessage {
public SystemMessage(
string title, string body,
Maybe<Attachment> attachment = default(Maybe<Attachment>)) {
}
}
public class Program
{
public static void Main() {
new SystemMessage("foo", "bar");
new SystemMessage("foo", "bar", new Attachment());
}
}
As we can see every Java’ish advice must be taken with a grain of salt.
Fortunately for us there is more and more C# posts about
using Maybe<T>
. For example this one
from the author of CSharpFunctionalExtensions library.
Although I cannot provide you with a list of best practices,
I think I have gathered enough experience to provide
you with a list of Maybe<T>
code smells:
- Nested
Maybe
s are wrong, for exampleMaybe<Maybe<string>>
. Usually this is a sign that you should replace one ofMap
calls by aFlatMap
(alternatively aSelect
by aSelectMany
call). Maybe
s that wrap collections are wrong, for exampleMaybe<List<User>>
. Instead return a non-empty or empty collection.Maybe
s wrapping nullable types are wrong, for exampleMaybe<int?>
. Instead convert nullable typeT?
toMaybe<T>
. Even if you chosen library does not support such conversion out of the box, you may write an extension method that provides this functionality yourself:
public static Maybe<T> ToMaybe<T>(this T? value)
where T: struct
=> value.HasValue ? Maybe<T>.From(value.Value) : Maybe<T>.None;
- Nested callbacks when using
Maybe<T>
fluent interface are wrong. For example:
// BAD Fluent interface spaghetti
var cultureBad = user.Select(
u => LoadPreferences(u)
.Select(prefs => prefs.Culture));
// GOOD, only one operation per Select method call
var cultureGood = user
.Select(u => LoadPreferences(u))
.Select(prefs => prefs.Culture);
Also be mindful when using Maybe<T>
with properties.
A declaration like:
public Maybe<string> Culture { get; set; }
Means that you may both get and set an optional value.
In other words assignment of None
value to this
property should be valid:
foo.Culture = Maybe<string>.None;
A property that can return None
but must be
set to some value should be split into a getter and a method:
// AVOID:
Maybe<string> Culture {
get { return Maybe<string>.From(_culture); }
set {
if (value.HasNoValue)
throw new ArgumentException("Culture cannot be empty.");
_culture = value.Value;
}
}
// PREFER:
Maybe<string> Culture
=> Maybe<string>.From(_culture);
void SetCulture(string culture) {
if (culture == null)
throw new ArgumentException("Culture cannot be empty.");
_culture = culture;
}
As you probably heard C# 8.0 is going to introduce a nullable reference types (NRT).
Will NRT replace Maybe<T>
?
For “Category 1” programmers, NRTs offer a better
alternative to Maybe<T>
. On the other hand a lot of people
that start in “Category 1”, slowly begin to embrace more FP approach.
Usually people start by using Maybe<T>
fluent interface to transform
one Maybe<T>
value into another. After some time they take a leap and
switch to writing in a more functional fashion.
We may also take a look at Kotlin,
a language created by JetBrains that from the very beginning
offered nullable reference types.
And yet the most popular Kotlin functional library
funKTionale
still offers an Option
type.
So it looks like NRT or not, Maybe<T>
is going to stay with us for sure.
Category 2: Wants to embrace FP paradigm
Programmers belonging to this category embraced FP. They often think about using F# at work and are a bit disappointed by poor C# pattern matching facilities.
Code written in FP fashion will never use if
to check
if Maybe<T>
contains some value, instead a fluent interface
will be used to transform Maybe<T>
s into some other values, for example:
private UserRepository _userRepository;
// NOTICE: No if's in code
public static Option<string> GetUserCulture(int userId)
=> _userRepository.FindById(userId)
.BiMap(
Some: user => GetUserCulture(user),
None: _ => null);
public static string GetUserCulture(User user)
=> user.Preferences
.Map(prefs => prefs.Culture)
.IfNone("en-US");
public class UserRepository {
private readonly List<User> _users = new List<User> {
new User(1, null),
new User(2, new UserPreferences {
Culture = "pl-PL"
})
};
// NOTICE: Extra extension methods for IEnumerable<T>
// that return Option<T> instead of null's.
public Option<User> FindById(int userId) {
return _users.Find(u => u.UserId == userId);
}
}
public class User {
public int UserId { get; }
private UserPreferences _preferences;
public Option<UserPreferences> Preferences
=> Optional(_preferences);
public User(int userId, UserPreferences preferences) {
UserId = userId;
_preferences = preferences;
}
}
public class UserPreferences {
public string Culture { get; set; }
}
Alternatively we may change our previous method to:
public static string GetUserCulture2(int userId)
=> _userRepository.FindById(userId)
.Bind(user => user.Preferences) // FlatMap
.Map(prefs => prefs.Culture)
.IfNone("en-US"); // default culture
The only difference between these two methods
is the value returned for users not present in the repository.
GetUserCulture
returns for them None()
but GetUserCulture2
returns a default culture (Some("en-US")
).
Another sign of a functional design, is that monads like Maybe<T>
will be unpacked only on the outskirts of the application.
For example in a typical RESTful service, Maybe<T>
value will
be unpacked only in the controller:
// GET /user/{userId}/culture
public IActionResult Get(int userId) {
// WARNING: In real apps do not return bare strings
// from the REST api.
// Always wrap them in DTOs / ViewModels / QueryResponses.
Maybe<string> culture = GetUserCulture();
return Maybe(culture);
}
// Presumably in the base controller
public IActionResult Maybe<T>(Maybe<T> m)
=> m.Map(value => Ok(value))
.IfNone(() => NotFound());
In this category of programmers there is also a small group of zealots, that in my opinion go a little bit to far in their cult of monads. They propose to use LINQ query syntax to transform monads. Let my explain this using an example:
// We want to sum three Option<int> values.
private static Option<int> GetOptionalInt()
=> 3;
// Using fluent interface:
var sum = GetOptionalInt()
.SelectMany(_ => GetOptionalInt(), (a,b) => a+b)
.SelectMany(_ => GetOptionalInt(), (a,b) => a+b);
// Using LINQ query:
var sum2 = from n1 in GetOptionalInt()
from n2 in GetOptionalInt()
from n3 in GetOptionalInt()
select n1+n2+n3;
I must admin that LINQ query offers some advantages like
ability to use let
and where
keywords. Also some transformations
may be easier to express using LINQ query syntax, for example:
var sum2 =
from n1 in GetOptionalInt()
from n2 in GetOptionalInt()
from n3 in GetOptionalInt()
let max = Math.Max(n1, Math.Max(n2, n3))
let min = Math.Min(n1, Math.Min(n2, n3))
where min != 0
select max / min;
Yet in my opinion fluent interface is in 90% of cases
a more readable and understandable way
to transform Maybe<T>
s and other monads.
For example we may rewrite the last code snippet to:
var res = Combine(
GetOptionalInt(),
GetOptionalInt(),
GetOptionalInt()
)
.Map(nnn => {
(var n1, var n2, var n3) = nnn;
return new {
Max = Math.Max(n1, Math.Max(n2, n3)),
Min = Math.Min(n1, Math.Min(n2, n3))
};
})
.Filter(m => m.Min != 0)
.Map(m => m.Max / m.Min);
// We need a few utils
private static Option<(T,T)> Combine<T>(Option<T> a, Option<T> b)
=> a.SelectMany(
_ => b,
(aValue, bValue) => (aValue, bValue));
private static Option<(T,T,T)> Combine<T>(Option<T> a, Option<T> b, Option<T> c)
=> Combine(a, b)
.SelectMany(
_ => c,
(tt, cValue) => (tt.Item1, tt.Item2, cValue));
Not as pretty as LINQ query but still readable.
At the end of the day consistency is what matters on real projects. Choose one style and follow it consistently.
In this category we find libraries like LanguageExt. This library has many flaws but still it is the best functional library on the market. My biggest disappointment with LanguageExt is poor documentation, which basically consists of just a list of functions without any guidelines how this library should be used and how it affects overall architecture. Compare this with Vavr (the most popular FP library for JVM) and you can clearly see the difference.
If you decided that you want to use FP in you code, you should definitively check awesome Railway oriented programming talk.
Maybe<T>
is not the only monad that is popular, other
frequently used one is Either<L,R>
.
Either<L,R>
is used to represent either a result of computation or an error.
You may think of Either<L,R>
as a functional response to exceptions.
If you want to use Maybe<T>
efficiently, you must learn
how it can be transformed it into other monads, in particular into Either<L,R>
.
E.g. we may make our last example more robust if we provide
information to the user why the computation failed:
var result = Combine(
GetOptionalInt(),
GetOptionalInt(),
GetOptionalInt()
)
.Map(ttt => {
(var n1, var n2, var n3) = ttt;
return new {
Max = Math.Max(n1, Math.Max(n2, n3)),
Min = Math.Min(n1, Math.Min(n2, n3))
};
})
.ToEither(() => Error("Not all values are available."))
.Bind(m => Divide(m.Max, m.Min)); // FlatMap
Either<Error, int> Divide(int a, int b) {
if (b == 0) return Left(Error("Cannot divide by zero"));
return Right(a / b);
}
// Helper classes:
public class Error {
public string Message { get; }
public Error(string message) {
Message = message;
}
public override string ToString()
=> $"Error({Message})";
}
public class ErrorHelpers {
// For `using static` import...
public static Error Error(string message)
=> new Error(message);
}
Since FP is on the rise, you will find a lot of books, blogs, podcasts and MOOC’s about using FP in C#. Also .NET has amazing F# community that is very welcoming to the beginners. One of the best blogs about FP in C# is in my opinion Mark Seemann blog.
What to do with None?
How much value you will be able to extract from Maybe<T>
depends on
your attitude towards None
s.
Every time when you have to handle None
, you must decide if it is
the result of
the accidental complexity
e.g. someone passed a wrong id to the REST API)
or if you just discovered a new edge case in your domain.
To better understand the problem let’s follow an imaginary example.
Joe must write a simple function that will
send an email message to all users whose subscriptions will end in the next month.
During implementation Joe notices that EmailAddress
field in User
entity
is declared as Option<Email>
:
public class User {
public Option<Email> EmailAddress { get; }
// ...
}
Now Joe knows that for some strange reason not all users have email addresses. Joe logs into production DB to confirm that some email addresses are missing and indeed they are. Looks like Joe just discovered a new edge case. Joe goes to Mark a business analyst to describe the problem. Mark is a long timer in the company and knows that for a short period of time users were able to log into the platform using their phone numbers instead of emails. A new solution is created. Users that have no email address will receive a text message instead of an email. Also users without email will be asked to enter their email address next time they log into the platform. Success!
On the other hand consider what will happen if Joe just
dig out the email address from Maybe<T>
by accessing Value
or if he just
log a warning about missing email address
without telling anyone from the business side?
End of the part I
Soon I will write a follow up to this post in which
we will try to implement a perfect Maybe<T>
type on our own and we will see
that it is not an easy task in C#.