Functional programming languages frequently contain a data type that is either an error or some useful result, like
Either<Error, T> or just
Error<T> (assuming C#-style generics). Focusing on the error is one way, but I’m more optimistic and call this
Result<T>. As so often, I miss this data type from the functional world in C#, so here is my take on it. I do not often feel the need to use something like
Result but right now I am working on a highly concurrent piece of software that is using a database as its datastore and for business reasons that database needs to have a global lock (sorry). It was originally meant to be a quick-and-dirty project, but this lock means that any kind of error reporting should only happen once the lock has been released again in order to keep the lock-times per operation as low as possible. Given that the database will likely be small enough to fit into RAM in its entirety, sending out error messages would wreck the performance of an otherwise innocent operation.
Result here is used to collect errors and return them to a handler than can operate when the lock has been released1.
We will need two different versions of
Result: A generic version
Result<T> and a non-generic version
Result for situations where you just want to return success or an error. They should be light-weight and easy to use, ideally without adding any explicit types whenever they are used. Errors are just error messages as a string, not exceptions.
I’m not actually that interested in giving you the whole code (you can find it here), I would rather explain some of the decisions I made when designing these types.
- There are three types
Failure. All of them are structs to avoid unnecessary allocations. This means among other things that
Result<T>cannot inherit from
Resultand I consider this a good thing. This unfortunately means that you cannot use C#’s pattern matching on it without using
- Whenever you need to express a failure of type
Result<T>, you can simply call
Result.Fail(string msg). There is no need to specify the actual type
Tthat you want to get a result for. This is achieved by using the
Failuretype (containing only the error message) and adding implicit casts from
- Similarly, when combining functions that return
Results, you often need to convert a failure
Result<S> sto a failure
Result<T> t. This is cumbersome, so there is a property
Failurethat simply returns a
Failureobject with the same error message. Implicit casting does the rest:
Result<T> t = s.Failure;.
- Constructing a success of any type
Result.Success(T value). You never have to write something like
- There is no implicit cast from
Result. This is on purpose. I found that other developers often wanted to return
Result<string>in a method that had return type
Result. Implicit casts that lose information should be avoided whenever possible (= always). If you want to downcast, you can use the
I’m torn on whether to put the various overloads for
Bind into its own static class and make them extensions (as it is) or to have them defined in the structs themselves. I find the extensions approach much more readable, but there is a semantic difference: With extensions, the structs are passed by value. Methods defined within the struct however are taking
this by refence, which is potentially worrisome if
Result<T> is used with large value-types
Some people might think that this is a good use-case for exceptions, but I disagree: These errors are down to invalid requests sent by the user and we just happen to need data from the database to validate them. It is by no means an exceptional situation but rather a case that is expected to occur frequently. ↩