Railway Oriented Programming
What Problem are we trying to solve?
Other Error Handling systems:
-
Exceptions
-
callback(err, result)
-
Default Values
-
Result Objects
Exceptions
Basically a goto statement with fancy syntax.
Means every line of code could be doing something that you couldn't expect.
Most often the simplest implementation.
Main Issue:
Magic Code Paths
callback(err, result)
Used by the NodeJs environment
You have to check if the err exists before using the result.
Errors must be handled at every level.
Main Issue:
Too much boilerplate
Default Values
If in doubt return null (or 0, 1970/01/01..)
Option<'a> isn't perfect either for this.
Main Issue:
No explanation of error
Result Objects
If in doubt return null (or 0, 1970/01/01..)
Option<'a> isn't perfect for this either
Main Issue:
Doesn't force user to handle error
public class Result<T> {
public bool HasError { get; set; }
public T Value { get; set; }
public string Error { get; set; }
}
Alternatives?
The Either Type
Abstracts a Left and a Right value
type Either<'a, 'b> =
| Left of 'a
| Right of 'b
Main Issue:
What side is the error value?
The Left!
What do we want?
-
Simple Semantics
-
Unambiguous
-
Defer Errors
-
Honest APIs
-
Explicit Consumption
Better Choice?
The Result Type
is a union of a Ok and Error type
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Error handling Phases
-
Construction
-
Pipelining
-
Consumption
Construction
let construction value =
match System.Int32.TryParse(value) with
| (true, int) -> Ok int
| _ -> sprintf "Could not parse value %s" value |> Error
API is string -> Result<int, string>
Pipelining
let pipeline value =
value
|> parseInt
|> Result.bind (fun x ->
if x > 10 then
Ok x
else
Error "Value must be greater than 10"
)
|> Result.map (fun x -> x * 2)
|> Result.mapError (fun error -> sprintf "Error: %s" error)
API is string -> Result<int, string>
Map
Result<a', 'error> -> a' -> 'b -> Result<'b, 'error> is the API
Takes a mapping function and invokes if Ok
Bind
Result<a', 'error> -> a' -> Result<'b, 'error> -> Result<'b, 'error>
is the API
If the value is Ok invokes a construction function.
Map Error
Result<a', 'error> -> 'error-> 'errorResult -> Result<'a, 'errorResult>
is the API
If the value is Error invokes a mapping function.
Consumption
let consumption value =
match value |> pipeline with
| Ok ok -> ok
| Error error -> failwith error
Using pattern matching to deconstruct the result similar to any other union.
Much more to talk about..
This is just a small glimpse into the things that can be achieved with this approach
-
Computation Expressions
-
Result Aggregation
-
Tee (Side Effects)
-
Kleisli Composition
Railway Oriented Programming
By Ryan Kelly
Railway Oriented Programming
Functional Error Handling
- 800