Lifting values and returning something instead of nothing.
Some<T>(T) -> Option<T>
To lift a value into the world of Option<T>
the main functions we use are all called Some.
The following snippets all do the same thing, which is wrap a value in Some<T>
(you should be able to run this code in C# interactive):
In each case the variable option
is of type Option<int>
, but specifically Some<int>
, which means you can do the following:
This is nice and simple, but what if your value comes from another function? Wouldn't it be nice if you could pass a Func<T>
instead of a T
, and wouldn't it be nice if it caught any exceptions for you, so don't have to wrap it in a try..catch
block?
Yes, it would.
But first, we need to explore how we return None<T>
None<T>(IMsg) -> Option<T>
Where Some<T>
wraps a value, None<T>
is a null-safe way of representing no value, with a helpful reason why there is no value: some input was invalid, an exception occurred, and so on. Note that we _always_** need to give a reason for a None<T>
** - another key principle in the world of Option<T>
.
Let's dive right in!
My pattern is for each class I write to have a Msg
static class, which contains all the messages relating to that class. My practice is to implement my messages as record
types - you don't have to do that, unless you want to use the built-in message types from the Jeebs
NuGet package.
(If your message has a public parameterless constructor you can use the function None<T, IMsg>() -> Option<T>
to create your None<T>
.)
Jeebs.Option
comes with two message interfaces: IMsg
(which has no properties or methods) and IExceptionMsg
(which has one property: the exception). We'll explore exception handling in the next section - but another way of writing the code block above would be like this:
Now we've seen IExceptionMsg
we can turn to the other Return function, which takes a Func<T>
instead of a T
.
Some<T>(Func<T>, Handler) -> Option<T>
A key principle of Option<T>
is that we always handle our exceptions. Therefore whenever we try to 'lift' a function instead of a value into the world of Option<T>
, we need to catch things that go wrong.
This is where the delegate F.OptionF.Handler
comes in, well, handy. Here is the definition of Handler
:
It takes an Exception
and returns an IExceptionMsg
. The handler is used by Return<T>(Func<T>, Handler)
, which creates a None<T>
and adds the reason message created by the handler.
So, this snippet does exactly what the Divide(int, int) -> Option<int>
function did in the previous example, but without the try..catch
block:
Messages are a simple but incredibly powerful way of describing everything that can go wrong in your system. You could have your own namespace for them all, but I prefer to define the messages right next to the class that uses them.
To realise the true power of Messages, you need to be disciplined about never reusing them (or only rarely - I reuse mine only when I have two versions of the same function, for example sync / async).
This means:
when you log a None<T>
with its Reason, you know exactly where the problem occurred
if you want to provide user feedback and translations, you can have specific error messages based on where the problem occurred
The messages we've used so far have been pretty simple, but here is an example of one from Jeebs.Data.Mapping
:
This message captures various important pieces of information:
the type T
is the type of the entity being updated
Method
is the name of the update method
Id
is the ID of the entity being updated
UpdateErrorMsg<T>
extends the LogMsg
abstract record from the Jeebs
package to set the log level, and provide a custom log message using the update values. Then in the Update()
function I can do something like this:
In that last code snippet you may have True
. That isn't a typo - there are two properties of F.OptionF
:
F.OptionF.True
which returns Some<bool>
with Value true
F.OptionF.False
which returns Some<bool>
with Value false
They are identical to writing Some(true)
or Some(false)
- but I like the shorthands.
They exist because you don't want to return a None<bool>
when something fails - you want to return a Some<bool>
with a false
value, so you can continue processing. This is what F.OptionF.False
is for (or simply False
if you have using static F.OptionF
).
Key principles and concepts of the Option<T> type.
Option<T>
What if you could get away from huge classes with monster methods? What if you didn't have to worry about handling null values?
What if you could wrap values in a consistent way, have helpful feedback when something goes wrong, and chain small reusable testable reliable functions to provide stable functionality?
You can: in the world of Option<T>
!
To whet your appetite for what using Option<T>
enables you to write, check out the following snippets - you'll notice that Option<T>
supports mixing sync and async functions.
The first is from my website's BlogController
. Each of the GetXXX(query)
methods returns Option<T>
, which are joined together via a Linq SelectMany
, and used to build the model for the List view.
Or this, which builds a SQL query, executes it, and then processes the results:
In both situations there cannot be an unhandled exception, there cannot be any null
values, and if something does go wrong an object with a helpful error message is return.
In the world of Option<T>
the following key principles are followed:
the return type is always Option<T>
(rather than Some<T>
or None<T>
)
if a function returns Option<T>
it means all exceptions have been handled
when returning None<T>
we must give a reason
everything that can go wrong in your system has an IMsg
which describes it
once in the async world, we must stay in the async world
F# contains a built-in Option
type, and I wanted to be able to code in that style but using C#. Over a year or so I have designed Option<T>
to mimic some of F#'s behaviour using C# way. It works best when you chain pure (and small) functions together - and if you use it well your exception handler will have very little to do!
The following serves as an introduction to the concepts behind Option<T>
which are likely to be a little alien to most C# developers. However, if you want to skip ahead straight to the code, feel free!
Option<T>
and all its extension methods live in the Jeebs
namespace (and some useful features in Jeebs.Linq
). However the actual 'pure' functions live in F.OptionF
- I put all my functional-style code in the F
namespace, and add an F
suffix to the class type. My preferred style is then to add using static F.OptionF
so the functions can be accessed directly.
'Pure' functions have no effect outside themselves. In other words, they receive an input, act on it, and return an output. They don't affect state, and they don't affect the input object.
In the OO world of C#, I'll admit, this is odd. There's not really any such thing as a 'function' - at least not one that exists outside a class definition. However as far as I can, Option<T>
is written as a series of pure functions, so even the methods in the Option<T>
class and the extension methods are simply wrappers for the functions, which all live in the F.OptionF
namespace.
Some<T>
and None<T>
Option<T>
is an abstract class with two implementations. The return type for a function is always Option<T>
, but the actual object will be one of these:
Some<T>
which contains a Value
property of type T
None<T>
which contains a Reason
property of type IMsg?
Think of None<T>
as a more useful nullable, because it comes with a reason why it has no value.
Within the Jeebs library - and I encourage you to follow the same discipline if you decide to you Option<T>
, the contract is that if a function returns Option<T>
it has handled all its exceptions so you don't have to.
This is a critical part of the usefulness of Option<T>
, and to be honest if you prefer having and handling exceptions I suggest you stop reading! However, I do believe there is a better way...
To enter the world we need to 'lift' values into it so we can benefit from all its features.
As Option<T>
is a mix of OO and functional programming styles I will be using the following notation across the documentation: function signature -> return type
, for example AddTwoNumbers(int, int) -> int
. This is partly for brevity, and partly because it is similar to how function signatures are written in F#, which will become useful as the functions get more complex.
All the code snippets are tested in LINQPad (v6.12, with .NET 5 support). If you want to try them out for yourself:
Add Jeebs.Option
NuGet package (Ctrl+Shift+P)
Add the following namespaces to the query (Ctrl+Shift+M):
Use an already-Optional function in a chain.
Like Map
, the Bind
functions receive the value of the input Option<T>
if it's a Some<T>
, and are bypassed if it's a None<T>
.
However, there are two important differences:
Bind
functions return an Option<T>
(Map
functions return T
which is wrapped by Some
).
Therefore they are expected to handle their own exceptions - one of our key principles is the contract that if a function returns Option<T>
its exceptions have been handled. (The method signature therefore does not have a Handler
in it.)
That said, you can be naughty because your binding function is still wrapped in a try..catch
block. However all exceptions caught in that block will return None<T>
with a Msg.UnhandledExceptionMsg
as the Reason.
Bind<U>(Func<T, Option<U>>) -> Option<U>
Bind
does a switch
and behaves like this:
if the input Option<T>
is None<T>
, return None<U>
with the original Reason
if the input Option<T>
is Some<T>
...
get the Value T
use that as the input and execute Func<T, Option<U>>
, then return the result
catch any unhandled exceptions using DefaultHandler
See it in action here:
Bind comes into its own in more complex scenarios, for example data access:
If db.GetCustomer()
fails, the next two operations are skipped, AuditSwitch
is run with the none
option, and a None<OrderPlacedModel>
is returned with the reason from db.GetCustomer()
.
You can of course also return Tuples instead of single values, and C# is clever enough to give Tuples automatic names now as well:
In the next section we will actually make a simple test app with everything you need to play around with Map
and Bind
. Before then here is an example of what this can look like in practice:
Here we have two potentially problem-ridden methods - returning a complex SQL query from a given input and executing a database query - that are now pure functions: notice they are declared static
. This is best practice, think of it like Dependency Injection but functional. The functions require no state, and interact with nothing else, so given the same input they will always return the same output.
This means they can be tested easily, and once they work correctly they will always work in the same way so you can rely on them.
You will also notice the use of the -Async
variations of Map
and Bind
- we will come to these later, but for now note that although x.ToList()
is not doing anything asynchronously, because the input is a Task<Option<T>>
we must use the -Async
variant. This leads to another of our key principles: once we are in the async world we stay in the async world.
Use a non-Optional function in a chain.
Once we have used Some<T>(T) -> Option<T>
to lift a value into the world of Option<T>
, we need to do something with it by chaining our functions together. The Option<T>
class comes with a load of methods which are actually wrappers for the functions that do the work.
So, although you can use the functions directly, you'll find it much more convenient to write chains using the common C# fluent syntax. These two are functionally identical:
In that snippet, functional
and fluent
are both Some<string>
with Value "42"
.
From now on, I will give function signatures as their equivalent method on Option<T>
because that should help keep things a little clearer, and it's what you will actually use.
Map<U>(Func<T, U>, Handler) -> Option<U>
Map
does a switch
and behaves like this:
if the input Option<T>
is None<T>
, return None<U>
with the original Reason
if the input Option<T>
is Some<T>
...
get the Value T
use that as the input and execute Func<T, U>
, then wrap the result in Some<U>
catch any exceptions using Handler
See it in action here:
If you are using LINQPad to to you change the second Map
in that sample so you have x / 0
instead of x / 2
and re-run the snippet, you will see that you still end up with an Option<string>
, only this time it's None<string>
with a DivisionFailedMsg
as the Reason.
There are two additional things to note here:
DefaultHandler
is available for you to use, but you should use it sparingly, and not rely on it, unless you really don't care about having helpful messages.
AuditSwitch(Action<T> some, Action<IMsg> none)
is useful for logging - the 'some' action receives the current Value (if the input is Some<T>
) and the 'none' action receives the current Reason (if the input is None<T>
).
If you haven't done any functional programming I suggest you read through the following, but if you want to see what Option<T>
can do, you can go straight to .
The other inspiration behind the type is Scott Wlaschin's . I have limited experience of functional programming, so my Option<T>
is not an implementation of his work, but it contains some similar ideas: in particular the 'failure' or 'sad' path, whereby if one function fails, the rest are skipped over.
This is identical to the previous snippet at the end of the section - except there are no exception handling options. So, if you change x / 2
to x / 0
you will get an UnhandledExceptionMsg
.