Functional DDD with C# Part 3: the elevated world

Introduction
In the two previous articles, we talk about the fundamentals of functional programming and functional architecture. But before moving to functional DDD, we need to talk about another fundamental of functional programming: the elevated world.
What is the elevated world?
In functional programming, the elevated world is a world in which objects are wrapped in a container Elevated<T> or, more concisely, E<T>. You unknowingly use the elevated world when you deal with asynchronous programming: Task<T>.
So we can write:
int -> E<int>string -> E<string>
And so on.
What is it for?
The elevated world has a lot of interests. The main one is that it makes it easier to chain functions.
Chaining functions bring out what software does: a succession of operations that transforms data into other data. Halas, too often, we first think about implementation details instead of thinking about a flow: structures, classes, interfaces, inheritance, algorithms, data tables and so many other details in which we drown.
Let's see how the elevated world helps us with functions chaining with a LINQ example. The following code gets the average of the 5 best scores. Look how comprehensive the program is using function chaining:
var scores = new List<int> { 12, 18, 17, 14, 13, 20, 8, 9, 14 };
var averageOf5BestScores = scores.OrderByDescending(x => x)
.Take(5)
.Average();
Console.WriteLine(averageOf5BestScores); //16.6
This chaining works because each function returns a value in the elevated world of enumerable integers: IEnumerable<int>.
Writing functions that chain well
Writing a function that chains well requires 4 criteria:
It must be pure. If it produces side effects, it will be complicated to chain other functions;
It must take an input parameter in the elevated world, for example,
IEnumerable<T>;It must be defined as an extension method of the input value (
this IEnumerable<T>);And it must return a result in an elevated world (
IEnumerable<T>).
Let's take an example using the previous example:
I define a method
TakeTopValuesOrderedByDescending()that extendsIEnumerable<T>. It has no side effects.It returns an
IEnumerable<T>to chain calls of other functions.
public static class IEnumerableExtensions
{
public static IEnumerable<T> TakeTopValuesOrderedByDescending<T>(this IEnumerable<T> values, int top)
{
return values
.OrderByDescending(m => m)
.Take(top);
}
}
Now I can rewrite the very first example by replacing the calls to OrderByDescending() and Take() with TakeTopValuesSortedByDescending():
var scores = new List<int> { 12, 18, 17, 14, 13, 20, 8, 9, 14 };
var averageOf5BestScores = scores
.TakeTopValuesOrderedByDescending(5)
.Average();
Console.WriteLine(averageOf5BestScores); //16.6
Working with the normal and the elevated world
During your work, you will have to jump from the normal world to the elevated world and vice-versa.
This is done by 3 types of operations:
An operation called
Returnin functional programming, which raises a value from the normal world to the elevated world;An operation called
Matchto move back from the elevated world to the normal world and get the embedded value;An operation to chain function calls in the elevated world:
Bind,MaporApply.

Writing our elevated world: Option<T>
Let's take an example and let's write our first elevated world: Option<T>. It is a very useful and very well-known pattern embedded in F# to model the absence of data (instead of using null which is a terrible idea).
A first definition
Option<T> has two possible values: Some and None. Here is its first definition:
public record Option<T>
{
public readonly bool IsSome;
private readonly T? _value;
//a private constructor to force the use of static constructors (see below)
private Option(T v, bool isSome) => (_value, IsSome) = (v, isSome);
}
Next, we need to write the 3 basic operations described before: elevating to Option<T>, returning from Option<T> and getting its inner value, and a chaining-functions helper Bind() or Map().
Operation 1: elevating a value to Option<T>
To elevate a value t to Option<T>, we will write two functions: Some(T t) and None().
First, we add two static constructors:
internal static Option<T> CreateSome(T v) => new(v, true);
internal static Option<T> CreateNone() => new(default!, false);
But they are not practical to use (Option.CreateSome(value)), that is why I keep them internal. I want to write Some(value). So let's do a small helper static class:
public static class Option
{
public static Option<T> Some<T>(T value) => Option<T>.CreateSome(value);
public static Option<T> None<T>() => Option<T>.CreateNone();
}
Now we have two "Return" functions to elevate a value to Option<T>. Here is how to use them:
//define some value
Option<string> someString = Some("foo");
//define the absence of value, instead of using null
Option<string> none = None<string>();
Operation 2: returning to the normal world
Next, we need to move back to the normal world and read the inner value of Option<T>. We will use a Match function. It unwraps the inner value of Option<T> and fires 2 functions that are set as parameters: one if the value is defined and the other one if it is not.
public R Match<R>(Func<R> None, Func<T, R> Some) => IsSome ? Some(_value!) : None();
Here is how to use it. Important: the returned value of provided functions for Some and None cases must be of the same type (string in this example).
public static string GetOptionValue<T>(Option<T> value)
{
return value.Match(
None: () => "Empty",
Some: (value) => $"{value}");
}
Operation 3: chaining functions in the elevated world
We need a generic mechanism to chain operations in the elevated world. In the world of Option<T>, chaining operations must be done only if the inner value of Option<T> is defined. This will be the purpose of the Bind() function.
It takes 2 parameters :
A value in the elevated world (
Option<T>in our example);And a function that operates in the normal world
Tand returns another value in the world ofOption. The returned value wrapped inOption<>can be of another type (Rfor "result type"): the signature of this function is thereforeT => Option<R>.
So here is the definition of Bind() for Option<T>:
public static Option<R> Bind<T, R>(this Option<T> option, Func<T, Option<R>> func)
{
return option.Match(
Some: v => func(v),
None: () => None<R>());
}
What we see is that Bind applies func on the inner value of option only if it exists. Else, the result will be None<R>.
Let's give it a try :
[TestMethod]
public void When_binding_a_AddOne_function_on_some_zero_value_then_the_result_is_one()
{
var addOne = (int i) => Some(i + 1);
var sut = Some(0);
var result = sut.Bind(addOne)
.Match(
Some: v => v,
None: () => 0);
Assert.AreEqual(1, result);
}
Now let's test the chaining:
[TestMethod]
public void When_binding_three_AddOne_functions_on_some_zero_value_then_the_result_is_three()
{
var addOne = (int i) => Some(i + 1);
var sut = Some(0);
var result = sut.Bind(addOne)
.Bind(addOne)
.Bind(addOne)
.Match(
Some: v => v,
None: () => 0);
Assert.AreEqual(3, result);
}
But what if one of the chained functions returns a None because something wrong happened? Well, the final result of the chain will be a None because Bind applies the parameterized function only if the value of Option<T> is Some.
[TestMethod]
public void When_binding_a_function_that_returns_none_then_the_result_is_none()
{
var addOne = (int i) => Some(i + 1);
var funcNone = (int i) => None<int>();
var sut = Some(0);
var result = sut.Bind(addOne)
.Bind(funcNone)
.Bind(addOne);
Assert.IsFalse(result.IsSome);
}
What is great is that Bind uses a "railway" management of problems: if one function in the chain returns a None, then the final result will be None. This mechanism is extremely well explained by Scott Wlaschin here. Managing errors in the domain become then very easy because you don't have to write any conditional logic. We will use it a lot in the application layer of our functional architecture.
What about Map()?
Map() is the same than Bind() instead that it takes as a parameter a function that returns a value in the normal world. As a result, it needs to lift it the elevated world.
Map() for Option<T> looks like this:
public static Option<R> Map<T, R>(this Option<T> option, Func<T, R> func)
{
return option.Match(
Some: v => Some(func(v)), // Lifting func's result in the elevated world
None: () => None<R>());
}
Stick to the elevated world as long as you can
Given the power of the Bind and Map to chain functions and their efficient way of handling errors, I highly recommend sticking to the elevated world for as long as you can.
Get the code
You can get Option<T> and its unit tests in my Grenat.Functional.DDD library here.
Summary
The elevated world makes it easier to write functions that can be chained together.
3 types of operations allow you to work between the elevated world and the real world:
A
Returnoperation (SomeandNonein this example) to raise a value from the normal world to the elevated world.A
Matchoperation to return to the normal world.Operations to chain functions in the elevated world:
Bind,Map,Apply.
BindandMapare great to manage errors without having to write conditional logic.Stay in the elevated world as long as you can to benefit from the power of
BindandMap.
About the cover image
Light pillars on the city of Kiruna in Swedish Lapland. This phenomenon of the polar regions is caused by the reflection of the intense light sources of the cities in the millions of ambient air crystals.





