Railway-oriented programming (ROP)
Railway-oriented programming (ROP) is a functional programming approach to handling errors and data flow. It uses the analogy of a railway track to represent the possible outcomes of a function:
- On-track (success): The function executes successfully and produces a valid output.
- Off-track (failure): The function encounters an error and produces an error message.
Similar to how a train can only be on one track at a time, an ROP function can only return either a success or a failure. This enforces clarity and simplifies error handling in your code.
Getting started with ROP
Not all methods force you to throw exceptions. Exceptions are a better choice when you have unexpected behavior in your application. For expected cases, it is better to apply a functional programming technique called Railway-oriented programming.
Well, that is what I think about exceptions:
- It is Simple: Easy to implement, especially in languages with built-in exception handling.
- It is Disruptive: Disrupts the normal program flow, forcing code to jump to a catch block.
- Implicit: Well, that is my favorite argument. Errors can be hidden within the code, making debugging harder.
- Not composable: Difficult to chain functions together while handling errors elegantly.
- Stack trace: Exceptions provide stack trace information which is valuable and adds +1 point to exceptions
- Latest .NET release: Microsoft is actively working on exceptions, especially for the latest versions they made really good progress in making exceptions to use fewer resources and be fast.
What went wrong?
Having exceptions makes your code dishonest. The dishonest code requires more investigation, it is hard to understand it at first look, and it doesn’t provide a “valid” understanding when you read the signature of the method. I always prefer honest signatures for my methods. Because it makes my code more understandable, readable and requires less documentation; well, at least, it doesn’t force you to read documentation whenever you are faced with a new method.
PS: Download our repo to follow the examples: GitHub - TuralSuleymani/DecodeBytes at tutorial/result_pattern
Here is our code
using Common.Repository;
using Domain.Models;
using Repository.Abstraction;
using Service.Abstaction;
using System;
using System.Threading.Tasks;
namespace Service.Core
{
public class AccountService : IAccountService
{
private readonly ICustomerService _customerService;
private readonly IUnitOfWork _unitOfWork;
private readonly IAccountRepository _accountRepository;
public AccountService(
ICustomerService customerService,
IAccountRepository accountRepository,
IUnitOfWork unitOfWork)
{
_customerService = customerService;
_unitOfWork = unitOfWork;
_accountRepository = accountRepository;
}
public async Task<Account> AddAsync(Account account)
{
bool isCustomerExist = await _customerService.IsExistsAsync(account.CustomerId);
if (!isCustomerExist)
{
throw new Exception("Customer with given Id doesn't exist");
}
else
{
bool isAccountExist = await _accountRepository.IsExistsAsync(account.AccountNumber);
if (isAccountExist)
{
await _unitOfWork.AddAsync(account);
await _unitOfWork.SaveChangesAsync();
return account;
}
else
{
throw new Exception("Account with given number doesn't exist");
}
}
}
}
}
What do you think about the code above?
Everything seems ok but when it comes to reading and understanding the code from the signature it is really hard to understand what this method is planning to return:
async Task<Account> AddAsync(Account account)
Is it always Account?
After delving into the source code, we see that it may return some exceptions depending on the values. It makes our signature dishonest. To work further with this method, we need to wrap it into try..catch and handle exceptions properly.
But how to deal with it?
One of the best choices to work with such a type of code snippet is to rewrite it and adopt a functional programming technique called Railway-oriented programming.
Railway Oriented Programming (ROP) is a technique for handling errors in C# that borrows ideas from functional programming. Here are some benefits of using ROP in C# applications:
- Improved Code Readability: With ROP, error handling becomes explicit. Your code follows a clear path, either on the "success track" processing data, or the "error track" indicating a problem. This can make code easier to understand and reason about.
- Composable Functions: ROP functions are designed to be chained together. This allows you to create pipelines of operations where each step can either succeed and move to the next step, or fail and halt the process. This modularity can improve code maintainability.
- Reduced Nested Conditionals: Traditional error handling often involves nested if statements to check for errors at each step. ROP avoids this by explicitly returning error information, leading to cleaner and less error-prone code.
- Functional Programming Benefits: While C# is object-oriented, ROP allows you to leverage some functional programming concepts like immutability. This can lead to code that is easier to test and reason about.
- Method signature honestly: It is always really easy to understand honest functions. You have a clear understanding of inputs and outputs and that makes you not use try..catch.
Note. It's important that ROP isn't a silver bullet. Here are some things to consider:
- Learning Curve: If you're new to functional concepts, ROP might require some additional learning effort.
- Overengineering: Not all situations require the complexity of ROP. For simpler error handling, traditional techniques might be sufficient.
Conclusion
Overall, ROP is a valuable tool for handling errors in C# applications, particularly when dealing with complex workflows or error-handling logic. If you're looking to improve code readability, and composability, and leverage some functional programming concepts, ROP is worth exploring.
In our next tutorial, we will implement the above code using the ROP Result pattern.