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