Error handling is important but can make an elegant algorithm into a mess.
Railway Oriented Programming (ROP
)
is used to make error handling elegant and composable.
Consider the simple function f
:
let tryParse s =
let b, v = System.Int32.TryParse s
if b then Some v else None
let f (g : string option) : float option =
match g with
| None -> None
| Some s ->
match tryParse s with // Parses string to int
| None -> None
| Some v when v < 0 -> None // Checks that int is greater than 0
| Some v -> v |> float |> Some // Maps int to float
The purpose of f
is to parse the input string
value (if there is Some
)
into an int
. If the int
is greater than 0
we cast it into a float
. In all
other cases we bail out with None
.
Although, an extremely simple function the nested match
decrease readability
significantly.
ROP
observes we have two kind of execution paths in our program
Some
valueNone
Since the error paths are more frequent they tend to take over the code. We would like that the happy path code is the most visible code path.
An equivalent function g
using ROP
could look like this:
let g (v : string option) : float option =
v
|> Option.bind tryParse // Parses string to int
|> Option.filter ((<) 0) // Checks that int is greater than 0
|> Option.map float // Maps int to float
It looks a lot like how we tend to process lists and sequences in F#
.
One can see an Option<'T>
like a List<'T>
that only may contain 0
or 1
element
where Option.bind
behaves like List.pick
(conceptually Option.bind
maps
better to List.collect
but List.pick
might be easier to understand).
bind
, filter
and map
handles the error paths and g
only contain the happy
path code.
All functions that directly accepts Option<_>
and returns Option<_>
are
directly composable with |>
and >>
.
ROP
therefore increases readability and composability.