Introduction
True Myth provides standard, type-safe wrappers and helper functions to help you with two extremely common cases in programming:
- not having a value — which it solves with a
Maybe
type and associated helper functions and methods - having a result where you need to deal with either success or failure — which it solves
with a
Result
type and associated helper functions and methods
You could implement all of these yourself – it’s not hard! – but it’s much easier to just have one extremely well-tested library you can use everywhere to solve this problem once and for all.
Even better to get one of these with no runtime overhead for using it other than the very small cost of some little container objects — which we get by leaning hard on the type system in C♯.
Aside: If you’re familiar with LanguageExt, you'll see that this has a lot in common with it — its main differences are:
- True Myth has a much smaller API surface than LanguageExt
- True Myth aims to be much more approachable for people who aren’t already super familiar with functional programming concepts and jargon
Maybe
Sometimes you don’t have a value. In C♯ (and .NET generally), we usually represent that
with a null
- either directly or by a Nullable<T>
— and then trying to program
defensively in the places we think we might get null
as arguments to our functions. For
example, imagine an endpoint which returns a JSON payload shaped like this:
{
"hopefullyAString": "Hello!"
}
But sometimes it might come over like this:
{
"hopefullyAString": null
}
Or even like this:
{}
Assume we were doing something simple, like logging the length of whatever string was there or logging a default value if it was absent. In typical C♯ we’d write something like this:
void LogValue(PayloadDto payload)
{
var length = payload?.hopefullyAString?.Length();
loger.Debug("Payload length: {length}", length);
}
async Task RequestFromApi()
{
await client.FetchFromApi()
.ContinueWith(payload => {
LogValue(payload);
// other stuff with payload ...
});
}
This isn’t a big deal right here… but — and this is a big deal — we have to remember to do
this everywhere we interact with this payload. The property hopefullyAString
can always
be null
everywhere we interact with it, anywhere in our program. 😬
Maybe
is our escape hatch. If, instead of just naively interacting with the payload, we do a very small amount of work
up front to normalize the data and use a Maybe
instead of passing around null
values, we can operate safely on the
data throughout our application. If we have something, we get Maybe
called Just — as in, “What’s in this field?
Just a string” or “Just the string ‘hello’”. If there’s nothing there, we have a Maybe
called Nothing. Maybe
is
a wrapper type that holds the actual value in it, and Just and Nothing are the valid states for that type.
You’ll never get a NullReferenceException
("object reference not set to an instance of an object") when trying to use
it!
Importantly, you can do a bunch of neat things with a Maybe
instance without checking whether it’s a Nothing or a
Just. For example, if you want to double a number if it’s present and do nothing if it isn’t, you can use the
Maybe.Map
function:
var hereIsANumber = Maybe.Of(42); // Maybe<int>
var hereIsNothing = Maybe<int>.Nothing;
int doubleFn = n => n * 2;
hereIsANumber.Map(doubleFn); // Just 84
hereIsNothing.Map(doubleFn); // Nothing
There are a lot of those helper functions and methods! Just about any way you would need to interact with a Maybe is there.
So now that we have a little idea what Maybe
is for and how to use it, here’s that same
example, but rewritten to normalize the payload using a Maybe
instance. We’re using C♯,
so we will get a compiler error if we don’t handle any of these cases right — or if we try
to use the value at hopefullyAString
directly after we’ve normalized it!
class PayloadDto
{
public string HopefullyAString { get; set; }
}
class Payload
{
public Maybe<string> HopefullyAString { get; set; }
}
async Task<Payload> Normalize(PayloadDto dto) =>
return Task.FromResult(new Payload {
HopefullyAString = Maybe.Of(dto.HopefullyAString)
});
void LogValue(Payload payload)
{
var length = payload.HopefullyAString.MapReturn(s => s.Length, 0);
loger.Debug("Payload length: {length}", length);
}
async Task RequestFromApi()
{
await client.FetchFromApi()
.ContinueWith(Normalize)
.ContinueWith(LogValue);
}
Now, you might be thinking, Sure, but we could get the same effect by just supplying a default value when we
deserialize the data. That’s true, you could! Here, for example, you could just normalize it to an empty string. And of
course, if just supplying a default value at the API boundary is the right move, you can still do that. Maybe
is
another tool in your toolbox, not something you’re obligated to use everywhere you can.
However, sometimes there isn’t a single correct default value to use at the API boundary. You might need to handle that
missing data in a variety of ways throughout your application. For example, what if you need to treat “no value”
distinctly from “there’s a value present, and it’s an empty string”? That’s where Maybe
comes in handy.
Result
Another common scenario we find ourselves in is dealing with operations which might fail. The most common pattern in .NET for dealing with this: exceptions. There are major problems with exception, especially around reusability and composability.
Exceptions are unpredictable: you can’t know whether a given function invocation is going to throw an exception until runtime as someone calling the function. No big deal if it’s a small application and one person wrote all the code, but with even a few thousand lines of code or two developers, it’s very easy to miss that. And then this happens:
// in one part of the codebase
object GetMeAValue(sring url) {
if (IsMalformed(url)) {
throw new Exception($"The url `{url}` is malformed!");
}
// do something else to load data from the URL
return data;
}
string RenderHtml(object toRender) {
// if toRender can't generate valid HTML, throw Error("invalid HTML");
// if it can, theRenderedHTML;
}
void WriteOutput(string html)
{
// I/O
}
// somewhere else in the codebase -- throws an exception
var badUrl = "http:/www.google.com"; // missing a slash
var response = GetMeAValue(badUrl); // throws here
// we never get here, but it could throw too
var htmlForPage = RenderHtml(value);
// so we definitely can't get here safely
WriteOutput(htmlForPage);
Notice: there’s no way for the caller to know that the function will throw. Perhaps you’re very disciplined and write good docstrings for every function – and moreover, perhaps everyone’s editor shows it to them and they pay attention to that briefly-available popover. More likely, though, this exception throws at runtime and probably as a result of user-entered data – and then you’re chasing down the problem through error logs. More, if you do want to account for the reality that any function anywhere in C♯ might actually throw, you’re going to write something like this:
try
{
var badUrl = "http:/www.google.com"; // missing a slash
var response = GetMeAValue(badUrl); // throws here
// we never get here, but it could throw too
var htmlForPage = RenderHtml(value);
// so we definitely can't get here safely
WriteOutput(htmlForPage);
}
catch(Exception exn)
{
HandleErr(exn);
}
This kind of universal boilerplate works against the Don't Repeat Yourself principle, and C♯ can’t help you here! There's no type signatures to say “This throws an exception!”
Instead, we can use a Result
to get us a container type, much like Maybe
, to let us deal with this scenario. A
Result
is either an Ok wrapping around a value (like Just does) or an Err wrapping around some type
defining what went wrong (unlike Nothing, which has no contents).
Result<Payload, string> GetMeAValue(string url)
{
if (IsMalformed(url)) {
return Result<Payload, string>.Err($"The url '{url}' is malformed");
}
// do something else to load data from the url
return Result.Ok(data);
}
Result<string, string> RenderHtml(string toRender)
{
// if toRender can't generate valid HTML, return Err("invalid HTML");
// if it can, return Ok(theRenderedHTML);
}
void WriteOutput(string html)
{
}
// somewhere else in the codebase -- no exception this time!
var badUrl = "http:/www.google.com"; // missing a slash
// value = Err(The url '${http:/www.google.com}' is malformed)
var value = GetMeAValue(badUrl);
// htmlForPage = the same error! or, if it was Ok, could be a different
// `Err` (because of how `andThen` works).
var htmlForPage = value.AndThen(RenderHtml);
value.Match(
ok: html => WriteOutput(html.UnwrapOr(string.Empty));
err: reason => Alert($"Something went seriously wrong here! {reason}");
)
When we have a Result
instance, we can perform tons of operations on whether it’s Ok or Err, just as we could
with a Just and Nothing, until we need the value. Maybe that’s right away. Maybe we don’t need it until
somewhere else deep in our application! Either way, we can deal with it easily enough, and have type safety throughout!