Type Driven Development: Railway Oriented Programming

Written by

in

Part 20 – Railway Oriented Programming

This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.

Parts
  1. Part 1 – Branded Types
  2. Part 2 – Product Types
  3. Part 3 – Union & Discriminated Unions
  4. Part 4 – Non-Empty Collections
  5. Part 5 – Indexed Types
  6. Part 6 – unknown vs any
  7. Part 7 – Result
  8. Part 8 – Schema
  9. Part 9 – Total Function
  10. Part 10 – Errors as Values
  11. Part 11 – Property Tests
  12. Part 12 – Type Proofs
  13. Part 13 – Exhaustiveness Checking
  14. Part 14 – Parse, Don’t Validate
  15. Part 15 – Anti-Corruption Layer
  16. Part 16 – Opaque Types
  17. Part 17 – Maybe
  18. Part 18 – Smart Constructors
  19. Part 19 – Pipeline
  20. Part 20 – Railway Oriented Programming
  21. Part 21 – Typestate
  22. Part 22 – Capabilities
  23. Part 23 – Immutability
  24. Part 24 – Making Impossible States Impossible
  25. Part 25 – Type Driven Development: How to do it
  26. Part 26 – Final Thoughts

The issue with using Pipeline programming is it only works for happy paths or data transformations. For everything else, there are failures/Result<T, E>’s involved, and it’s quite painful to open them up and do logic on each function in turn.

safeFetch(...)
.then(r => {

  // if everything is ok, take out the value,
  // and continue the pipeline with parsing
  if(r.isOk()) {
    return safeParse(r.value, schema)

  // otherwise, keep returning the error result
  // so it'll pop out at the end
  } else {
    return r
  }
})Code language: JavaScript (javascript)

Even if 9 of our functions are pure and have no errors, if we connect it to a 10th that could have a failure, now the entire pipeline could fail.

This is where Railway Oriented Programming comes in. You design a Pipeline where there are 2 tracks; the green happy path, and the red unhappy path, made specifically for the Result type. If any function returns an Err<E> type, it does not process the next function in the pipeline, and instead, just returns the err(E) value, basically short circuiting the rest. Otherwise, if everything is good as an Ok<T>, it keeps passing the value through the Pipeline as normal.

safeFetch(...)
.andThen( safeParse )
.andThen( cowSchema )Code language: CSS (css)

This solves a lot of problems:

  • unlike Promise, it doesn’t throw exceptions; your Pipeline can continue to be a total function
  • you can define the error type the pipeline can return, Promise is just Promise<T> vs. Result<T, E>
  • all Result<T, E> style functions can suddenly be composed together easily
  • functions that can’t fail, you can just return an ok() with their return value

You get all the benefits of Pipeline, with type-safe error handling. This powerful abstraction is how you build larger functions, and in turn an entire program that is type-safe.

That said, you have 2 escape hatches needed for the real world. The first is _unsafeUnwrap/_unsafeUnwrapErr for unit tests so you don’t have to type narrow to get the value/error out just to test something. Also comes with a linter to ensure you aren’t using _unsafe* functions in production code.

cow1 = ok(1)._unsafeUnwrap()
// will explode if it were an err('b00m')Code language: JavaScript (javascript)

The 2nd is .match; say for example you’re at the top of an AWS Lambda and need to return an HTTP response as JSON, or throw if you’re panicking:

result.match(
  data => ({
    statusCode: 200, 
    body: JSON.stringify(data) 
  }),

  (error:CustomError | Panic) => {
    if(error.tag == 'CustomError'){
      return ({ 
        statusCode: 500, 
        body: error.message 
      })
    }
    throw error
)Code language: JavaScript (javascript)

For TypeScript, you have Neverthrow or Effect and there are options for other languages, if it’s not built-in, to make Railway Programming easier.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *