In this post, we're going to look at a somewhat functional way of programming, in which we combine functional programming with object-oriented programming. We owe this method, or pattern, to Scott Wlaschin, its original author—you can find more information on his website fsharpforfunandprofit.
The first thing we need to understand before starting is how our code works in a normal use case. For this example, we'll simulate the use case of creating a new user in the system.
And it's important to note: you do NOT need experience with functional programming to apply this pattern.
Index
1 - Paths to follow
A - Happy path
As always, we start our design by thinking about the correct path to complete a task. In our case, it's pretty straightforward: we validate the user, save the user, and send a verification code to their email.
public string BasicAccountCreation(Account account){ ValidateNewAccount(account); SaveUser(account); SendCode(account); return "Usuario anadido correctamente";}
B - Non-happy path
Of course, users don't always follow "the rules"—or maybe they're already registered. In these cases, we have to handle errors to avoid, for example, having duplicate users in our database.
As we can see, we may encounter different errors at different points in our application. Here is a typical object-oriented implementation for error handling:
public string BasicAccountCreation(Account account){ string accountValidated = ValidateNewAccount(account); if(!string.IsNullOrWhiteSpace(accountValidated)) { return accountValidated; } bool isSaved = SaveUser(account); if(!isSaved) { return "Error actualizando la base de datos"; } bool isSent = SendCode(account); if(!isSent) { return "Error Enviando el email"; } return "Usuario anadido correctamente";}
As you can see, it's a lot of code—easy enough to follow here, but if we had many more validations, it could become quite complex.
If we analyze these flows in their respective steps, we see that the result is as follows: For the happy path, everything goes well, so we have a single entry and exit point.
Whereas for error analysis, we have several breaking points in our code.
As mentioned at the beginning of the post, the main idea of this pattern is to blend the best of functional programming into our object-oriented applications.
To do this, we'll analyze the same process, but this time doing a functional flow analysis.
This is the functional representation. Notice that the flow is similar but not the same, since in this scenario we have not just one response, but two at the end of our process.
2 - Functional design
To make our application return two results, what we need to do is have an object that holds both objects.
But, before we start coding like crazy, we have to understand the steps to follow, because there could be not just a single error, but several.
Based on our "non-happy path," we have the following path:
As you can see, there are three types of errors: a validation error, a database error, and a mail server error.
So the resulting object would look something like the following, an object inside containing both "success
" and the errors.
type result = Success || ErrorValidacion || ErrorBaseDeDatos || ErrorSMTPServer
Obviously, this code isn't final, since it's focused exclusively on this specific use case. We can compress this even more if we group all errors in a list or an error object that contains a list—our object would then only have two properties, independent of the use case.
type result = Success || Error
This solution is not 100% correct, though, as we return only a success indicator but none of the values, only the failures. If we want to return the element, we need to implement generics; this way, we can define Success<T>
, where T
is the type we want to return.
Type result<T> = Success of T || Error
3 - Pattern objective
Once we understand the pattern and its objective, we can define several goals:
- Combine all errors in a single place.
- Create smaller functions.
- Display the data flow.
- A single function per use case.
4 - How the Result Pattern (Railway Oriented Programming) works
It's also important to understand how it works. In step two, we saw the main objective is to create a wrapper for the type we want to return (Result<T>
)—but why?
Here's where the functional programming part comes in. Basically, Result<T>
will cause the type T
to constantly change (or not, depending on the logic). To make this clearer, imagine this analogy: You have a car, it enters a function, and a bike comes out. Then that bike goes into another function, and a boat comes out, for example.
It's simple: the input type of a function should be the output type of the previous function.
4.1 - Comparison
Compared to a normal flow that checks for errors and returns when needed—although you don't always return, the program simply does not continue but jumps back—in our new pattern, it does "continue" (in quotes, because what it really does is skip all subsequent functions after a failure).
To see a bit more detail, in every method we receive both input parameters—the correct one and the failure. As long as everything is fine, our function keeps executing each method normally, but if an error is thrown at any moment, it switches to the error part.
We see we have "two" inputs and two possible outcomes. Each connection between two methods is made through a binding, which controls whether we're in the happy path or the failure path.
5 - How to apply ROP in C#
The pattern is mainly based on a single class we'll call Result<T>
. So, the first thing we'll do is create a struct
that holds our Result<T>
and, of course, the errors.
Another thing to keep in mind is that, in our code, we may have methods that return void
. These methods aren't compatible with the new pattern, because, as we've seen, every method must return a type. For this particular case, we have the Unit
type, which does nothing and simulates void
.
public sealed class Unit{ public static readonly Unit Value = new Unit(); private Unit() { }}public struct Result<T>{ public readonly T Value; public static implicit operator Result<T>(T value) => new Result<T>(value); public readonly ImmutableArray<string> Errors; public bool Success => Errors.Length == 0; public Result(T value) { Value = value; Errors = ImmutableArray<string>.Empty; } public Result(ImmutableArray<string> errors) { if (errors.Length == 0) { throw new InvalidOperationException("debes indicar almenso un error"); } Value = default(T); Errors = errors; }}
If we look closely, Result<T>
contains the value of type T
and the errors.
Note: the errors are just a simple string for this example, but we could have an Error
type with codes, etc.
All the handling for this pattern is done using extension methods, allowing us to define when it's Success
or Failure
.
public static class Result{ public static readonly Unit Unit = Unit.Value; public static Result<T> Success<T>(this T value) => new Result<T>(value); public static Result<T> Failure<T>(ImmutableArray<string> errors) => new Result<T>(errors); public static Result<T> Failure<T>(string error) => new Result<T>(ImmutableArray.Create(error)); public static Result<Unit> Success() => new Result<Unit>(Unit); public static Result<Unit> Failure(ImmutableArray<string> errors) => new Result<Unit>(errors); public static Result<Unit> Failure(IEnumerable<string> errors) => new Result<Unit>(ImmutableArray.Create(errors.ToArray())); public static Result<Unit> Failure(string error) => new Result<Unit>(ImmutableArray.Create(error));}
There isn't much mystery to the code: just call Success
or Failure
as needed, and it creates the Result<T>
type.
5.1 - Setting up the environment for ROP
Now let's see how to create the method that will decide whether program execution should go through the happy path or skip methods and go straight to the end.
In this post, we'll see the creation of the .Bind()
method. For this, we'll use extension methods together with delegates, because the Bind
method will receive a delegate (in other words, a method) as a parameter.
public static class Result_Bind{ public static Result<U> Bind<T, U>(this Result<T> r, Func<T, Result<U>> method) { try { return r.Success ? method(r.Value) : Result.Failure<U>(r.Errors); } catch (Exception e) { ExceptionDispatchInfo.Capture(e).Throw(); throw; } }}
Looking at the code, we see it's basically a simple if expressed as a ternary operator: if r
(result
) is Success
(its error list is empty), run the method we pass as a parameter. Otherwise, it creates an object with the errors.
5.2 - Enhancing the environment for ROP
In this example, I haven't made the methods too complex—they currently work synchronously. If you want asynchronous support, you'd need to add Task<T>
to both the method return value and input parameters and, of course, await the result.
Or, for example, if you wanted to create a .Then()
method that ignores the delegate result (an Action
delegate in this case) and returns the input value.
Or a .Map()
where we map from one value to another.
public static async Task<Result<U>> Bind<T, U>(this Task<Result<T>> result, Func<T, Task<Result<U>>> method){ try { var r = await result; return r.Success ? await method(r.Value) : Result.Failure<U>(r.Errors); } catch (Exception e) { ExceptionDispatchInfo.Capture(e).Throw(); throw; }}public static Result<T> Then<T>(this Result<T> r, Action<T> action){ try { if (r.Success) { action(r.Value); } return r; } catch (Exception e) { ExceptionDispatchInfo.Capture(e).Throw(); throw; }}public static Result<U> Map<T, U>(this Result<T> r, Func<T, U> mapper){ try { return r.Success ? Result.Success(mapper(r.Value)) : Result.Failure<U>(r.Errors); } catch (Exception e) { ExceptionDispatchInfo.Capture(e).Throw(); throw; }}
6 - Use case
The code is available on github.
Note: the code is synchronous for simplicity.
6.1 - Creating a user using OOP
For the example, I've created a use case or service, to which we'll simulate adding a user in our database. First, we'll implement the service using object-oriented programming:
public interface IAddUserPOOServiceDependencies{ bool AddUser(UserAccount userAccount); bool EnviarCorreo(UserAccount userAccount);}/// <summary>/// Add the user using an object-oriented programming structure./// </summary>public class AddUserPOOService{ private readonly IAddUserPOOServiceDependencies _dependencies; public AddUserPOOService(IAddUserPOOServiceDependencies dependencies) { _dependencies = dependencies; } public string AddUser(UserAccount userAccount) { var validacionUsuario = ValidateUser(userAccount); if (!string.IsNullOrWhiteSpace(validacionUsuario)) { return validacionUsuario; } var addUserDB = AddUserToDatabase(userAccount); if (!string.IsNullOrWhiteSpace(addUserDB)) { return addUserDB; } var sendEmail = SendEmail(userAccount); if (!string.IsNullOrWhiteSpace(sendEmail)) { return sendEmail; } return "Usuario añadido correctamente"; } private string ValidateUser(UserAccount userAccount) { if (string.IsNullOrWhiteSpace(userAccount.FirstName)) return "El nombre propio no puede estar vacio"; if (string.IsNullOrWhiteSpace(userAccount.LastName)) return "El apellido propio no puede estar vacio"; if (string.IsNullOrWhiteSpace(userAccount.UserName)) return "El nombre de usuario no debe estar vacio"; return ""; } private string AddUserToDatabase(UserAccount userAccount) { if (!_dependencies.AddUser(userAccount)) { return "Error añadiendo el usuario en la base de datos"; } return ""; } private string SendEmail(UserAccount userAccount) { if (!_dependencies.EnviarCorreo(userAccount)) { return "Error enviando el correo al email del usuario"; } return ""; }}
As you can see, we have several problems. First, by returning a single type (a string, in this case), we limit the values we can return—we want to check several validations but can only return one at a time.
We could address this if, instead of returning a single string, we returned a List<string>
; but in that case, if everything works fine, we're creating a list when we only need a single value.
Besides that, the code is somewhat hard to read. In this use case, we only have to check a couple of things, but the class is already quite long and harder to read. Still, the code works, albeit not beautifully—the main method AddUser
isn't pretty but it does the job.
6.2 - Creating a user using ROP
Now let's do the same process, but using the "Railway Oriented Programming" pattern that we've covered throughout this post.
Before moving on, remember that this use case is intended to use our Result<T>
type. I've also adjusted the dependency interface a bit to implement our new pattern.
And here is our implementation:
public interface IAdduserROPServiceDependencies{ Result<bool> AddUser(UserAccount userAccount); Result<bool> EnviarCorreo(string email);}public class AdduserROPService{ private readonly IAdduserROPServiceDependencies _dependencies; public AdduserROPService(IAdduserROPServiceDependencies dependencies) { _dependencies = dependencies; } public Result<UserAccount> AddUser(UserAccount userAccount) { return ValidateUser(userAccount) .Bind(AddUserToDatabase) .Bind(SendEmail) .Map(_ => userAccount); } private Result<UserAccount> ValidateUser(UserAccount userAccount) { List<string> errores = new List<string>(); if (string.IsNullOrWhiteSpace(userAccount.FirstName)) errores.Add("El nombre propio no puede estar vacio"); if (string.IsNullOrWhiteSpace(userAccount.LastName)) errores.Add("El apellido propio no puede estar vacio"); if (string.IsNullOrWhiteSpace(userAccount.UserName)) errores.Add("El nombre de usuario no debe estar vacio"); return errores.Any() ? Result.Failure<UserAccount>(errores.ToImmutableArray()) : userAccount; } private Result<string> AddUserToDatabase(UserAccount userAccount) { return _dependencies.AddUser(userAccount) .Map(_ => userAccount.Email); } private Result<bool> SendEmail(string email) { return _dependencies.EnviarCorreo(email); }}
Before explaining the code in more detail, notice how much the amount of code has been reduced. Less code usually means a cleaner and easier-to-understand codebase.
The main AddUser
method is much smaller and easier to understand. Here, we're using our .Bind
method to call each of the functions we want, and at the end of the sequence, we call .Map
, which will run in the case everything went well, returning the user account.
The ValidateUser
method underwent the biggest change; since it's the first to run, it's where we create our Result<T>
—as Success
if everything is fine, or as Failure
if there are errors.
The AddUserToDatabase
and SendEmail
methods do the same as before, but now SendEmail
only receives a string email
; this is intentional, to show that the output of one method is the input to the next.
In these methods, we don't need to check for errors, because the dependency (interface) returns a Result<T>
, so if there are errors, they're handled by .Map()
or .Bind()
in the main function.
Conclusion
In this post, we've seen how to blend functional and object-oriented programming in a way that greatly helps with code cleanliness, especially in error handling.
Personally, I recommend this pattern as it makes programming much simpler. In fact, this entire website is written with this pattern. Using it has allowed me to have much cleaner code, and when I have to change something, it barely takes me any time to read and fully understand what the code is doing.
The code for this example is on GitHub—I invite you to take it and try it in one of your projects. Over the next days/weeks I'll be expanding its functionality, for example, to make all methods asynchronous. If you have any improvement ideas, feel free to submit a Pull Request.
If you liked this content, please share (menu on the left).
If there is any problem you can add a comment bellow or contact me in the website's contact form