Some and None
Lifting values and returning something instead of nothing.
Some<T>(T) -> Option<T>
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>
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>
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
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 occurredif 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 updatedMethod
is the name of the update methodId
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:
True and False
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 returnsSome<bool>
with Valuetrue
F.OptionF.False
which returnsSome<bool>
with Valuefalse
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
).
Last updated