Skip to content

mastratisi/railroad

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

railroad

A terse Haskell railroad error handling DSL abstracting over error functors by catamorphism via Either.

railroad is a tiny, opinionated library that brings Railway Oriented Programming (ROP) to Haskell. It gives you a clean, linear DSL for collapsing error-carrying values (Maybe, Either, Validation, Bool, and collections thereof) directly into your MonadError (or Effectful.Error) context — no noisy case expressions, no repeated either/maybe boilerplate.

Haskell License


Why railroad?

Typical error handling in Haskell quickly turns into a tarpit of accidental controlflow:

do
  user <- case fetchUser uid of
    Nothing -> throwError UserNotFound
    Just u  -> pure u

  cfg <- case loadConfig path of
    Left e  -> throwError (toConfigError e)
    Right c -> pure c

railroad replaces all of that with a single, consistent operator family that reads linearly:

do
  user <- fetchUser uid ? UserNotFound          -- Maybe / Either / Validation / Bool
  cfg  <- loadConfig path ?? toConfigError      -- custom error mapping
  items <- queryItems filters ?+ NoResultsFound -- cardinality guard

Install

Nix

Here is an expression you can add to ghcWithPackages:

railroad = pkgs.haskellPackages.callCabal2nix "railroad" (pkgs.fetchFromGitHub 
  { owner  = "mastratisi"
  ; repo   = "railroad"
  ; rev    = "master"
  ; sha256 = pkgs.lib.fakeSha256; # Nix will tell you the real hash on first build   
  }) {};

Hackage

The package is under the name railroad. Link: https://hackage.haskell.org/package/railroad

Operators

Operator Purpose Example
?? Main collapse with custom error mapping action ?? toMyError
? Collapse to constant error action ? MyError
?> Predicate guard val ?> isBad toErr
??~ Recover with a mapped default (error → value) action ??~ toDefaultVal
?~ Recover with a fixed default value action ?~ defaultVal
?+ Require non-empty collection items ?+ NoResults
?! Require exactly one element items ?! fromCardinalityErr
?∅ Require empty collection (alias ?@) items ?∅ DuplicateFound

All operators work uniformly on:

  • Bool
  • Maybe a
  • Either e a
  • Validation e a
  • Traversable containers of the above ([Maybe a], Vector (Either e a), …)

Design Highlights

  • Bifurcate typeclass turns any “railroad-shaped” functor into Either (CErr f) (CRes f) via catamorphism
  • Native support for Effectful (Error effect) and MTL (MonadError) via Railroad.MonadError
  • Short-circuiting for Either, error accumulation for Validation

See railroad.md for a deeper tour, or just read the code at Railroad.hs. It's a short file.

Comparison with hoist-error

Both hoist-error and Railroad are small, focused Haskell libraries that make error handling in MonadError (or Error effects) far more pleasant than raw case expressions, either, or maybe. They let you collapse partiality structures (Maybe, Either, etc.) into a linear monadic flow unpacking values while still controlling how errors are turned into your application’s error type.

They solve the same core pain point—“railroad-style” error handling where the happy path stays on the main track and errors branch off cleanly—but they do it with different levels of abstraction and power.

Quick Feature Comparison

Feature hoist-error Railroad
Core mechanism PluckError class + hoistError Bifurcate type family + catamorphism to Either
Supported functors Maybe, Either e, ExceptT e m Bool, Maybe, Either, Validation, traversables of any of the above
Collection handling None (manual) Built-in cardinality: ?+ (non-empty), ?! (exactly one), ?∅ (must be empty)
Error mapping Function or constant (<%?>, <?>) Function (??) or constant (?) with typed CErr constructors
Recovery / defaults No built-in ?~ / ??~ (constant or computed fallback)
Guards No ?> (predicate → error)
Monadic hoisting <%!?>, hoistErrorM Handled naturally by >>= + ??
Primary ecosystem Any MonadError / ExceptT / mtl Effectful Error (with full MonadError re-export)
Dependencies Minimal (base + mtl/transformers) effectful, validation, mtl
Learning curve Very low (just a few operators) Slightly higher (8 operators, but ?? and ? cover ~90%)
Size ~150 LOC ~150 LOC

When to choose which?

Choose hoist-error if you want:

  • The absolute smallest API and dependency footprint
  • Only need simple Maybe/Either/ExceptT hoisting
  • Classic mtl or transformers codebases

Choose Railroad if you want:

  • A full DSL that feels built into the language
  • Excellent Validation support (error accumulation)
  • Frequent collection checks or predicate guards
  • Maximum readability in Effectful applications

Contact

Feel free to DM me on https://x.com/mastratisi97 . I find it interesting to know if other people also have found this module useful.

About

A terse Haskell railroad error handling DSL abstracting over error functors by catamorphism via Either

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors