The original Railway Oriented Programming (ROP) blogpost can be found here: https://fsharpforfunandprofit.com/rop/
- ROP abstracts away error handling so we can pull it out of business logic
- ROP helps us remember to handle errors by failing to compile when we forget
- If you have a method than can fail, have it return a
Result<T>
instead- When your method succeeds return
Success<T>
- When your method fails, instead of throwing an exception or returning null, return
Failure<T>
instead
- When your method succeeds return
- ROP Logic methods
- Use
Then
whenever you have a method that returns aResult<T>
- Use
Map
when you have a method that doesn't return aResult<T>
- Use
OnSuccess
when you don't care what's returned
- Use
- ROP Error methods
- Use
Verify
when you want to to return an error when some predicate returns false - Use
VerifyNotNull
when you want to to return an error when some data is null - Use
MapFailure
when you want to change what kind of error a method is returning
- Use
- Other ROP methods
- Use
OnFailure
when you want to run some code only if a failure happend - Use
Always
when you want to run some code regardless if your code has failed or not - Use
MapTo
when you are done with ROP and you want the final result of your computation
- Use
ROP abstracts away error handling so we can pull it out of business logic.
Say we want to get the first element out of a list. We have to handle the scenario of of an empty list. We usually choose between throwing an exception or returning null. That's the design Linq took for First()
and FirstOrDefault()
. This isn't ideal because we'll have to remember to check for null
or put our code in a try-catch
block and if we forget our program could crash!
If we use ROP, we can write a method that returns a Result<T>
. Result<T>
is an abstract class with two subclasses, Failure<T>
and Success<T>
. Let's call that method FirstOrFailure()
and see how it compares to Linq's methods:
try {
item = data.First();
} catch {
/* Handle error */
}
/* Process item */
item = data.FirstOrDefault();
if( item == null ) {
/* Handle error */
};
/* Process item */
data.FirstOrFailure()
.Then( item => /* Process item */ );
Then
is a method that takes the data out of theResult
class and lets us operate on it. If an error has happened, it doesn't do the operation. We'll learn more about how to use it later.
In the FirstOrFailure()
example, the error handling section of the code is gone!
Another benefit of ROP is, to a certain degree, we are forced to handle the error case with FirstOrFailure()
.
Imagine that you have a list of Int
s and you want to get the first element, add 1
to it, and then send it to the function SendNumber()
but you forget to check if the list is empty:
// Won't compile
item = bunchOfNumbers.FirstOrFailure()
var plusOne = item + 1; // Oops, we forgot to check if bunchOfNumbers was empty!
SendNumber( plusOne )
This won't compile because item
is a Result<Int>
and you can't add 1
to it. You must write the logic in a way that unwraps the Int
. This (almost) forces you to consider the case where Result<Int>
is a Failure<Int>
.
bunchOfNumbers.FirstOrFailure()
.Map( item => item + 1 ) // The error case is handled for us
.Then( plusOne => sendNumber( plusOne ) );
We would have to go out of your way to write this method using FirstOrFailure()
in a way that would crash on an empty list.
Map
and Then
are methods defined on the Result<T>
class that help us tie actions together. We'll learn how they work next.
Then
is the power tool of ROP. If we wanted to, we could write all program logic in ROP with Then
statements. All other ROP logic methods can be considered helper functions. Then
ties statements together.
In functional programming,
Then
is sometimes calledBind
because it binds actions together,Result<T>
is called a monadic value, and methods that returnResult<T>
are called monadic functions. ROP is a specific instance of the monad design pattern.
Then
handles methods that fail. Use it whenever possible.
.Then( x => DoSomethingSometimesFails( x ) )
What if you have a method that doesn't return a Result<T>
? You could wrap the result of the method in a Result<T>
, and then it would fit in a Then
. A less verbose way to do the same thing is to use Map
. Map
is the same as Then
except it wraps the result in a Result<T>
.
.Map( x => DoSomething( x ) )
is the same thing as
.Then( x => DoSomething( x ).AsSuccessResult() )
If you have a method that returns a value that you don't care about use OnSuccess
.
OnSuccess
will throw away whatever is returned, and pass what was passed in to the next action.
.OnSuccess( x => DoSomething( x ) )
is the same as
.Then( x => DoSomething( x ).Map( _ => x ) )
or, if we want to write OnSuccess
only using Then
(just to prove to ourselves that we can)
.Then( x => DoSomething( x ).Then( _ => x.AsSuccessResult() ) )
Verify
is a way to explicitly set your method to error when some expression is true.
.Verify( number => number < 10, ErrorKey.NumberMustBeGreaterThanTen )
VerifyNotNull
will create an ArgumentNull
error when some variable is null
. The second parameter saves the name of the thing that was null
in the ErrorKey
for easier debugging.
.VerifyNotNull( payload, nameof( payload ) )
is an alias for
.Verify( _ => payload != null, ErrorKey.ArgumentNull( payload ) );
TODO
TODO
TODO
TODO
Unless it is outside the general scope i'd put in a short definition of what ROP actually is. I think i asked awhile ago and it is railway oriented programming, but that doesn't even show up on https://acronyms.thefreedictionary.com/ROP !