My goals in this talk:
I am highly indebted to Scott Wlaschin for coming up with the metaphor of Railway-Oriented Programming and doing such a great job taking us through it.
My understanding of ROP was derived from Scott and I recommend you dive into his work as well at https://fsharpforfunandprofit.com/rop/.
As a user, I want to update my name and e-mail address.
As a user, I want to update my name and e-mail address and see meaningful error messages on failure.
Operation | Error Case |
---|---|
Receive request | |
Validate and canonicalize request | Error if name is blank or e-mail address is not valid |
Update existing user record | Error if user is missing or the database operation fails |
Send verification e-mail | Error if there is an authorization error or a timeout |
Return result to user |
The "happy path" scenario for our process is straightforward.
As we think about handling errors, however, this gets messy!
How can this function have multiple outputs?
This works, but think about having hundreds of errors. Not sustainable!
How can this function have multiple outputs?
By the way, this is called a sum type or discriminated union. It's sort of like an enumeration but doesn't require that each element of the union be of the same base type.
How can this function have multiple outputs?
Much better, but how do we send information back to callers, especially on failure?
How can this function have multiple outputs?
On success, return a thing. On failure, return an error message.
How can this function have multiple outputs?
We can enhance this further by collecting and passing a list of messages.
We can move some of the failure checking in F# into the domain model. For example, suppose we lay out common errors:
We can also create some simple functions to determine whether we are on the Success or Failure track:
These allow us to create domain primitives, domain-level entities which describe facts about our world.
...so how do we do it?
The metaphor for this is a switch, breaking off into success and failure paths.
On success, we continue along the track. On failure, we move down to a separate failure track.
For multiple functions, we maintain the two tracks. We can move from success to failure, but not the other way.
So how does this work with three functions?
The same way that we connect two functions! We can chain together an indefinite number of functions this way.
Each operation contains a switch function: stay on the Success path or move to the Failure path.
Single-track composition is easy.
Two-track composition is easy.
But our functions are switches, which are not composable!
We need some sort of adapter block which transforms our switch into a two-track operation.
This function now composes successfully.
This also allows us to start with a non-ROP entity (such as a string from an external caller) and move it onto the rails:
Map + Tee = Composition
When dealing with exceptions, convert them to Failure
s.
Over the course of this talk, we have looked at the Railway-Oriented Programming metaphor for the Either monad. We have seen the basics of implementation, as well as how to solve several common problems with the process.
Be sure to check out Scott Wlaschin's full set of resources at https://fsharpforfunandprofit.com/rop/.
To learn more, go here:
https://csmore.info/on/rop
And for help, contact me:
feasel@catallaxyservices.com | @feaselkl
Catallaxy Services consulting:
https://CSmore.info/on/contact