A little while back I started paying attention to Rust and got all inspired to have a bit of that it C#. The result was this library I called Beefeater.
This library contains helpers to add semantics to the optionality of your parameters and results from method calls. I built it on NetStandard1.0
for maximum compatibility.
Some of this will soon become obsolete with C# 8 features such as the C# 8 nullable reference types, but much of it gives you the power you need right now.
You can install the NuGet package using Install-Package Beefeater
or by heading to the Nuget Package Page.
Last week I released version 0.5.0.
The basic idea is similar to the Nullable<T>
type that exists for value types. But extend this into semantic markup around method arguments and return types.
The approach is to allow you to write declarative code, and let the library determine which part gets called.
To make this seamless, I’ve leveraged a few features of C#, namely the Implicit Casts, and Generics.
But just declaring optionality isn’t enough, so the goal is to go further and provide functions to write better safer code using these types.
The bits
The library is made up of a few different utilities and ideas.
Option<T>
- declare a reference type as possibly null (C# 8 makes this obsolete, but implicit casts to/from the new syntax might work)NotNull<T>
- so you can declare something is certainly never null (or crash immediately rather than on dereference)Result<TValue, TError>
- Represents either a success result or an error (Modelled after Rust’s std::result)Either<TValue, TError>
- a more generic concept of a two-part Union type
There are implicit casts to and from (where sensible) these types when they are in use, so you only need to explicitly declare them in contracts, and occasionally in return types. Although, a bunch of work has been put in to avoid return types most of the type.
These classes have some consistent methods to help out:
- Match - a function or action that is provided lambda
Func
s/Action
s respectively to cover both known states of the object. - HasValue - similar to the
Nullable<T>
- ValueOrDefault - to ensure you certainly get back a value
- IsSuccess - a similar pattern to
Nullable<T>
applied toResult<,>
There was a theory of making an enum match but it wasn’t achievable. However again C# 8 might solve this with new pattern matching too.
There is also Resharper definitions provided to work with and be trusted by the Resharper Null static analysis.
Show me the code!
Option
As a simple example you may have a method like this:
public string Modify(string first, string second)
{
if (first == null)
{
throw new ArgumentNullException(nameof(first));
}
if (second == null)
{
return null;
}
return first + second;
}
Typical code. A bit of error checking, and some switch logic based on an optional argument.
I don’t know about you, but it is hard for me to know that first
is required, but second
is optional. At least not without comments, or a thorough inspection of the code. Shouldn’t the signature tell me this? (C# 8 says it will.)
Lets take a look at this using Beefeater:
public Option<string> Modify(NotNull<string> first, Option<string> second)
{
return second.Match(
v => first + v,
() => Option<string>.None);
}
var x = Modify("Hello", "World");
var y = Modify("Hello", null);
That is very clean to me. First, the signature tells us everything we need to know.
And when we look at the calling syntax, it is seamless. it works exactly as you expect. With one difference.
x
and y
in the example are both Option<string>
type. This means you cannot pass this to a method that requires NotNull<string>
, or even string
itself.
Why? Well, clearly you have no idea if that result is safe. You know it is either a Nothing or a non-null string. (Is it both? Schrodinger’s Option<T>
?)
The only sensible thing to do is to check, right? It would be unsafe not to. Thus the value of the library is revealed.
That check can use the Match
method, too.
var x = Modify("Hello", "World");
var outputMessage = x.Match(
some: value => $"The result is {value}",
none: () => "There wasn't anything to report");
Console.WriteLine(outputMessage);
Your code will start to become very functional, but you will also get compile-type checks to ensure you are handling error cases correctly.
If you really want to live dangerously, though, there is an explicit cast back to the original type, that will throw a PanicException
. This is basically the library equivalent to the NullReferenceException
, but should normally throw earlier in your code when the Null is first observed, rather than late at dereference.
Much like async/await, this library is contagious and will ripple through your code, just FYI.
Result<TValue, TError>
Apart from Option<T>
, the other most useful is the Result<TValue, TError>
.
Again, you might have written:
public bool Create(string filePath, string second)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
if(second == null)
{
return false;
}
using (FileStream stream = File.OpenWrite(filePath))
{
using (var writer = new StreamWriter(stream))
{
writer.WriteLine(second);
}
}
return true;
}
Again, a little cruft. Optionality unclear. But there is more. This method can and will throw a bunch of exceptions. FileNotFound, UnauthorizedAccess, and others. Your calling code is either going to handle many exceptions, or the app will crash. We could put the logic into the method, and the code would get complex. But how do we handle the return? returns true
, false
or Throws exception, how do you translate that into a concise return type of true
, false
or FileNotFound or Unauthorized or Other?
Beefeater says like this: (Rust too, actually…)
public enum ErrorResult
{
UnknownError,
FileNotFound,
Unauthorized
}
public Result<bool, ErrorResult> Create(NotNull<string> filePath, Option<string> second)
{
FileStream stream;
try
{
stream = File.OpenWrite(filePath);
}
catch (UnauthorizedAccessException ex)
{
return ErrorResult.Unauthorized;
}
catch (FileNotFoundException ex)
{
return ErrorResult.FileNotFound;
}
catch (Exception ex)
{
return ErrorResult.UnknownError;
}
using (stream)
{
return second.Match(
v =>
{
using (var writer = new StreamWriter(stream))
{
writer.WriteLine(v);
}
return true;
},
() => false);
}
}
To me, the business logic is very easy to read from this code. Again the signature is clear what to expect and how to use the result.
var result = Create("myfile.txt", someVar);
var outputMessage = result.Match(
isCreated => isCreated
? "The file was created with someVar"
: "The file was not created because someVar",
error =>
{
switch(error)
{
case FileNotFound:
return "File Not Found";
case Unauthorized:
return "The file is secure and you can't get it.";
case UnknownError:
default:
return "An unknown Error occurred";
}
});
Console.WriteLine(outputMessage);
Just like that, every code-branch/edge-case has to be covered, or it will not compile. (Enum notwithstanding.)
Either
Either<T1, T2>
is very similar to Result, except the semantic meaning differs. It allows a method to return either one type or another type.
public Either<long, double> DivideByTwo(int aNumber)
{
if (aNumber % 2 == 0)
return aNumber / 2;
return aNumber / (double)2;
}
var x = DivideByTwo(10);
var x = DivideByTwo(5);
Recursion?
You could even nest these things:
Either<Option<string>, NotNull<int>>
Option<Either<Foo, Bar>>
Result<Option<string>,CustomError>
Result<Unit,string>
Different combinations will provide different value in different situations.
Unit
? Yeah. When you go functional, representing void
as a type is much helpful. I have another library where I get my Unit from, but there are literally dozens on Nuget.
C# 8
I’ve hung a lot of stuff on C# 8 in this article. At some point, I should cover what is coming (though at this stage nothing is certain). Basically, though, this library has been hanging around for a while, before nullable reference types was even being discussed publicly. So whether nullable reference types happens in 8, 8.1 or C# 9, this library is here today and can get you started thinking about different states of optionality in your code today.
Performance?
About that….
So I haven’t actually spent any time profiling to see if this is damaging your code. For most LOB apps, this will not be the bottleneck compared to IO. But it is a valid point and does need further investigation and optimisation.
From a development performance point of view, I do go slower to go faster. I think about and code up the edge cases up front, rather than doubling back when something goes wrong.
Attention!
So that’s Beefeater. I’ve started using it slowly on utilities and simple console apps. Hopefully, you find it interesting and it inspires you to think about how you handle your code yourself.
If you don’t like the ideas in this library, remember a Result<,>
is just a tuple, a (string, ErrorEnum)
if you wish. The rest is just boilerplate code you could write yourself. (Though I do handle all the error and state checking for you so you can just provide the Actions 😉 )