packages feed

railroad 0.1.1.5 → 0.1.2.0

raw patch · 7 files changed

+426/−31 lines, 7 filesdep ~validationPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: validation

API changes (from Hackage documentation)

+ Railroad: (?|) :: (Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b) => m a -> m b -> m (Either (CErr b) (CRes a))
+ Railroad: (?|<>) :: (Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b, CErr a ~ CErr b, Semigroup (CErr a)) => m a -> m b -> m (Either (CErr a) (CRes a))
+ Railroad.MonadError: (?|) :: (Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b) => m a -> m b -> m (Either (CErr b) (CRes a))
+ Railroad.MonadError: (?|<>) :: (Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b, CErr a ~ CErr b, Semigroup (CErr a)) => m a -> m b -> m (Either (CErr a) (CRes a))

Files

CHANGELOG.md view
@@ -1,5 +1,18 @@ # Revision history for railroad +## 0.1.2.0 -- 2026-10-11+* Add the fallback operator `(?|)`: runs the left action, and only if it fails runs the right one.+  The two sides can be different structures (`Maybe`, `Either`, ...) with the same success type, the+  last error is kept, and the result chains and is finished with `?` or `??`. It never throws, so+  `Railroad.MonadError` re-exports the one definition.+* Add `(?|<>)`, the same fallback but when every source fails the errors are combined with `<>`+  in source order. The sources must agree on the error type (`CErr`) and the success type (`CRes`).+* Documentation: a servant + effectful example and a section on the fallback operators in the README,+  clearer haddocks (module headers, `CErr`, `CRes`, each operator), and `readme.md` in `extra-doc-files`.+* Add `tested-with` (GHC 9.4.8 to 9.14.1, the versions the test suite is run on).+* Write the `validation` bound without a trailing zero or a strict lower bound:+  `(>= 1.1 && < 1.3) || (>= 1.3.1 && < 1.4)`, the same set of releases as in 0.1.1.5.+ ## 0.1.1.5 -- 2026-10-11 * Exclude `validation-1.3.0` from the bounds: it does not build (it uses `foldable1-classes-compat`   without declaring it). `validation >= 1.3.1` needs `base >= 4.18`, so on GHC 9.4 the solver picked
railroad.cabal view
@@ -1,13 +1,14 @@ cabal-version:      3.0 name:               railroad-version:            0.1.1.5+version:            0.1.2.0 license:            BSD-3-Clause license-file:       LICENSE author:             Frederik Kallstrup Mastratisi maintainer:         mastratisi@proton.me category:           Control, Error Handling build-type:         Simple-extra-doc-files:    CHANGELOG.md+tested-with:        GHC == 9.4.8, GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3, GHC == 9.12.4, GHC == 9.14.1+extra-doc-files:    CHANGELOG.md, readme.md synopsis:           A railway oriented mini-DSL for unified error handling. description:     A railway oriented mini-DSL for handling and interpreting error states @@ -24,7 +25,7 @@       base >=4.17 && < 4.23,       effectful >= 2.5 && < 2.8,       mtl >= 2.2 && < 2.4,-      validation (>= 1.1 && < 1.3.0) || (> 1.3.0 && < 1.4)+      validation (>= 1.1 && < 1.3) || (>= 1.3.1 && < 1.4)  library     import:           stuff
+ readme.md view
@@ -0,0 +1,247 @@+# railroad++**A terse Haskell railroad error handling DSL abstracting over error functors by catamorphism via `Either`.**++Instead of++```haskell+rows <- runQuery (userById uid) >>= either (const (throwError err503)) pure+user <- case rows of+  []  -> throwError err404+  [u] -> pure u+  _   -> throwError err500+```++write++```haskell+user <- runQuery (userById uid) ? err503 ?! cardinalityErr err404 (const err500)+```++Here `runQuery` returns `Eff es (Either DbError [row])`. Each operator+peels one layer (the database failure, then "exactly one row") and says+what to throw if it fails, so the happy path stays one line. A longer+[servant example](#example-a-servant-backend) is below.++The purpose of `railroad` is to error handle tersely, keeping the+happy-path clean of control-flow and functor unpacking.  It does this+with 10 combinators, that abstract over binary co-products (`Bool`,+`Maybe`, `Either`, `Validation`) and Traversables or Folds over them.++```haskell+import Railroad.MonadError+import Control.Monad.Except++example :: Either String Int+example = runExcept $ do+ -- False ? "error" would make the block short circuit to Left "error"+ + x <- pure (Just 2) ? "Value missing"+ + -- Right (Just 1) ~> Just 1 ~> 1+ y <- pure (Right $ Just 1) ? "Outer fail" ? "Inner fail"+ + -- [Just 4] ~> [4] ~> 4+ z <- pure [Just 4] ? "List failed" ?! const "Not a single element"+ + q <- pure Nothing ?~ 1 + + pure (x + y + z + q) -- ~> Right 8 +```++You just need to work in a monad which supports a `MonadError`-like+context. Either by being an instance of `MonadError` or in an Eff+stack with `Error`.++[![Haskell](https://img.shields.io/badge/language-Haskell-%23612a7e.svg)](https://www.haskell.org/)+[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)++---++## Install++### Nix++Here is an expression you can add to `ghcWithPackages`:+```nix+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++++## Example: a servant backend++Handlers in a servant + `effectful` backend (adapted from a real one)+are chains of queries and checks where each failure is an HTTP status.+The queries return `Eff es (Either DbError [row])`: a database failure+wrapping the rows. With `railroad`:++```haskell+getUser :: Error ServerError :> es => UserId -> Eff es UserDTO+getUser uid = do+  user <- runQuery (userById uid) ? err503 ?! cardinalityErr err404 (const err500)+  pure (toUserDTO user)++register :: Error ServerError :> es => Email -> Eff es Token+register email = do+  runQuery (userByEmail email) ? err503 ?∅ const err409   -- must not exist yet+  makeJWT email ? err500++updateAsset :: Error ServerError :> es => UserId -> Asset -> Eff es ()+updateAsset uid asset = do+  _ <- runQuery (permitted uid asset) ? err500 ? err403   -- db error, then "not permitted"+  ...+```++Each operator peels one layer, left to right: `? err503` unwraps the+`Either`, then `?!` demands exactly one row (none is a 404, several a+500). The same handlers in plain Haskell:++```haskell+getUser' uid = do+  rows <- runQuery (userById uid) >>= either (const (throwError err503)) pure+  user <- case rows of+    []  -> throwError err404+    [u] -> pure u+    _   -> throwError err500+  pure (toUserDTO user)++register' email = do+  rows <- runQuery (userByEmail email) >>= either (const (throwError err503)) pure+  unless (null rows) (throwError err409)+  makeJWT email >>= either (const (throwError err500)) pure++updateAsset' uid asset = do+  allowed <- runQuery (permitted uid asset) >>= either (const (throwError err500)) pure+  unless (and allowed) (throwError err403)+  ...+```++The failure paths no longer take up lines, and the order of the layers is+the order you read them in. Validating a request with a `Validation`+reports every problem at once: `pure (map validateFile files) ?? badRequest`+turns all the invalid files into one 400. `Railroad.MonadError` has the+same operators for plain `MonadError`, including servant's `Handler`.++## Understanding the Operators++| Operator | Purpose                                       | Example                       |+|----------|-----------------------------------------------|-------------------------------|+| `??`     | Derail on error with custom error mapping     | `action ?? toMyError`         |+| `?`      | Derail with constant error                    | `action ? MyError`            |+| `?>`     | Derail on predicate                           | `(action ?> isGood) toErr`    |+| `??~`    | Recover with a mapped default (error → value) | `action ??~ toDefaultVal`     |+| `?~`     | Recover with a const default value            | `action ?~ defaultVal`        |+| `?+`     | Derail on empty collection                    | `items ?+ NoResults`          |+| `?!`     | Derail if not exactly one element             | `items ?! fromCardinalityErr` |+| `?∅`     | Derail on non-empty collections (alias `?@`)  | `items ?∅ DuplicateFound`     |+| `?\|`    | Fall back to another action on error         | `cache k ?\| db k ? NotFound` |+| `?\|<>`  | Fall back, combining the errors with `<>`     | `a ?\|<> b ?? toErr`         |+++For the semantics of the operators there are 3 relevant questions:+What counts as an error?  What happens in the error case?  What+happens in the success case?+++### What counts as an error?+Here we can split the operators into two families. Those that+interpret errors on the structure of Functors and Traversable of+Functor. Those that interpret error on the cardinality of Foldables.++For the former, they all (`?`, `??`, `?~`, `??~`) interpret what+an error is the same way:++ | Functor          | Error Constructor    | Error Info (`CErr`) | Result Info (`CRes`) |+ |------------------|----------------------|---------------------|----------------------|+ | `Bool`           | `False`              | `()`                | `()`                 |+ | `Maybe a`        | `Nothing`            | `()`                | `a`                  |+ | `Either e a`     | `Left`               | `e`                 | `a`                  |+ | `Validation e a` | `Failure`            | `e`                 | `a`                  |+ | `t f`            | Any element is error | `CErr f`            | `t (CRes f)`         |+++For the latter each operator (`?+`, `?!`, `?∅`) interpret what+cardinalities of a Foldable counts as errors differently. To wit:++| Operator    | Success Condition   | Error Info               | Result Info |+|:------------|:--------------------|:-------------------------|:------------|+| `?+`        | Non-empty           | `()`                     | `t a`       |+| `?!`        | Exactly one element | `CardinalityError (t a)` | `a`         |+| `?∅` / `?@` | Is empty            | `t a`                    | `()`        |++Where+```haskell+-- | Structurally a @Maybe ta@ for cardinality failures+data CardinalityError ta = IsEmpty | TooMany ta++-- | Catamorphism for CardinalityError+cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e+```++### What happens in the error case?+When an error is detected, the railroad "derails":++1. The provided error-mapping function is applied to the **Error+   Info** (`CErr` or `CardinalityError`).+2. The result of the error mapping is thrown+3. This causes early termination of the monadic sequence.++**Recovery Operators (`?~`, `??~`):** Unlike the other operators,+these do **not** derail the computation. Instead, they provide a+"switch" to bring the logic back onto the happy path by providing a+default value through a recovery function.+++**Traversal case:** For `Bool`, `Maybe`, `Either` the error+accumulation is short-circuiting and stops at the first error. For +`t (Validation e a)` the semigroup constraint on `e` is used to+accumulate all errors together. For more information on accumulating+errors, see the+[validation](https://hackage.haskell.org/package/validation/docs/Data-Validation.html)+package on Hackage.+### What happens in the success case?+In the success case the result is unwrapped or validated (`?>`, `?+`)+and the monad continues its execution. Result Info in the above tables+shows the resulting type inside the monad.++### Fallback operators+`?|` and `?|<>` try one source after another and stop at the first+success. They are for sources that may be different structures (for+example a cache returning `Maybe` and a database returning `Either`)+with the same success type (`CRes`). The result is bifurcatable, so a+final `?` or `??` turns a total failure into a throw:++```haskell+cache k ?| db k ?| legacy k ? NotFound+```++`?|` keeps the last error. `?|<>` also needs the sources to share the+error type (`CErr`) and combines all the errors with `<>`, in source+order, when every source fails. Map differing error types to a common one+first, for example with `note`-like functions:++```haskell+(cache k <&> note [CacheMiss]) ?|<> (db k <&> first pure) ?? NotFound+```++Neither throws by itself, and the right-hand side only runs if the left+one failed. Unlike `Validation`, which runs all independent checks and+collects every failure, a fallback chain succeeds on the first good answer.++## More+See [`railroad.md`](railroad.md) for a deeper tour, or just read the+code at [`Railroad.hs`](src/Railroad.hs). It's a short file.++## 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.
src/Railroad.hs view
@@ -4,6 +4,10 @@ {-# LANGUAGE TypeFamilies         #-} {-# LANGUAGE UndecidableInstances #-} +-- |+-- Railway-oriented operators: turn a failure in a 'Bifurcate' structure (@Bool@, @Maybe@,+-- @Either@, @Validation@, traversables of them) or a bad cardinality into a throw in the+-- @Error@ effect, keeping the happy path clean. For @MonadError@ use "Railroad.MonadError". module Railroad where  import           Data.Bool               (bool)@@ -14,12 +18,16 @@ import           Effectful.Error.Dynamic (Error, throwError_)  +-- | What the failure case carries: @()@ for 'Bool' and 'Maybe', @e@ for 'Either' and+-- 'Validation', that of the element for a traversable. type family CErr f :: Type where   CErr Bool               = ()   CErr (Maybe a)          = ()   CErr (Either e a)       = e   CErr (Validation e a)   = e   CErr (t a)              = CErr a+-- | What the success case carries: @()@ for 'Bool', @a@ for 'Maybe', 'Either' and+-- 'Validation', @t (CRes a)@ for a traversable @t a@. type family CRes f :: Type where   CRes Bool               = ()   CRes (Maybe a)          = a@@ -60,45 +68,74 @@     => Bifurcate (t (Validation e a)) where   bifurcate = bifurcate . sequenceA --- | Collapses a structure into its inner type or an effectful error+-- | Unwraps the success case, or throws the failure mapped by the function. collapse :: (Error e :> es, Bifurcate f) => (CErr f -> e) -> f -> Eff es (CRes f) collapse toErr = either (throwError_ . toErr) pure . bifurcate  --- | Collapses a structure using an error mapper.+-- | Unwraps the success case, or throws the error info mapped by the function. (??) :: forall a es e. (Error e :> es, Bifurcate a) => Eff es a -> (CErr a -> e) -> Eff es (CRes a) action ?? toErr = action >>= collapse toErr --- | Collapses a structure using a constant error.+-- | Unwraps the success case, or throws a constant error. Chains to peel nested layers:+-- @m ? e1 ? e2@. (?) :: forall es e a. (Error e :> es, Bifurcate a) => Eff es a -> e -> Eff es (CRes a) action ? err = action ?? const err --- | Collapses any value based on a predicate+-- | Passes the value on if it satisfies the predicate, else throws the error built from+-- it: @(m ?> p) toErr@. (?>) :: forall es e a. (Error e :> es) => Eff es a -> (a -> Bool) -> (a -> e) -> Eff es a (?>) action predicate toErr = do   val <- action   if predicate val then pure val else throwError_ $ toErr val --- | Collapses a structure and recovers to a value dependent on the error+-- | Unwraps the success case, or recovers with a value computed from the error info. (??~) :: forall es a. Bifurcate a => Eff es a -> (CErr a -> CRes a) -> Eff es (CRes a) action ??~ defaultFunc = action >>= either (pure . defaultFunc) pure . bifurcate --- | Collapses a structure, and recovers to a constant default value in the error case+-- | Unwraps the success case, or recovers with a default value. (?~) :: forall es a. Bifurcate a => Eff es a -> CRes a -> Eff es (CRes a) action ?~ defaultVal = action ??~ (const defaultVal)  --- | @IsEmpty@ or @TooMany ta@ — structurally a @Maybe ta@ for cardinality failures+-- | Fallback: runs the right action only if the left fails. The sides may be different+-- structures with the same success type; the last error is kept. Finish with '?' or '??'.+--+-- > cache k ?| db k ?| legacy k ? NotFound+(?|) :: forall m a b. (Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b)+     => m a -> m b -> m (Either (CErr b) (CRes a))+ma ?| mb = do+  a <- ma+  case bifurcate a of+    Right x -> pure (Right x)+    Left _  -> bifurcate <$> mb++-- | Like '?|', but if every source fails their errors are combined with '<>', in order.+-- The sides must also share the error type; map it to a common one first.+(?|<>) :: forall m a b. ( Monad m, Bifurcate a, Bifurcate b, CRes a ~ CRes b+                        , CErr a ~ CErr b, Semigroup (CErr a) )+       => m a -> m b -> m (Either (CErr a) (CRes a))+ma ?|<> mb = do+  a <- ma+  case bifurcate a of+    Right x  -> pure (Right x)+    Left ea  -> do+      b <- mb+      pure $ case bifurcate b of+        Right y  -> Right y+        Left eb  -> Left (ea <> eb)++-- | Why a collection was not a single element: empty, or too many (carrying it). data CardinalityError ta = IsEmpty | TooMany ta --- | Catamorphism for CardinalityError+-- | Fold for 'CardinalityError': one result per case. cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e cardinalityErr onEmpty onTooMany = \case   IsEmpty -> onEmpty   TooMany xs -> onTooMany xs  --- |  Non-empty is success state. Returns the collection as-is.+-- | Succeeds if non-empty, returning the collection; else throws the constant error. (?+) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> e -> Eff es (t a) (?+) action err = do@@ -106,7 +143,8 @@   if null xs then throwError_ err else pure xs  --- | Single element is success state.  Returns the single element+-- | Succeeds if there is exactly one element, returning it; else throws the error built+-- from the 'CardinalityError'. (?!) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> (CardinalityError (t a) -> e) -> Eff es a (?!) action toErr = do@@ -117,17 +155,17 @@     _   -> throwError_ $ toErr $ TooMany xs  --- | Empty is success state. Returns Unit.+-- | Succeeds if empty, returning @()@; else throws the error built from the collection. (?∅) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> ((t a) -> e) -> Eff es () (?∅) action toErr = do   xs <- action   if null xs then pure () else throwError_ (toErr xs) --- | Non-unicode alias for `(?∅)`+-- | ASCII alias for '?∅'. (?@) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> ((t a) -> e) -> Eff es () (?@) = (?∅) -infixl  0 ??, ?, ??~, ?~, ?!, ?+, ?∅, ?@+infixl  0 ??, ?, ??~, ?~, ?!, ?+, ?∅, ?@, ?|, ?|<> infixl 1 ?>
src/Railroad/MonadError.hs view
@@ -1,9 +1,6 @@ -- |--- Re-implementation of Railroad operators ('??', '?', '?>', etc.)--- using 'MonadError' / 'throwError' instead of the Effectful 'Error' effect.------ Use this module in classic @mtl@ / @transformers@ / @ExceptT@ code.--- For Effectful prefer the versions from "Railroad".+-- The operators of "Railroad" for any 'MonadError' (@mtl@, @transformers@, @ExceptT@,+-- servant's @Handler@) instead of the @Error@ effect. In an @effectful@ stack use "Railroad". {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts    #-} @@ -19,44 +16,47 @@ import           Control.Monad.Except (MonadError (..)) import           Data.Foldable        (toList) --- | Collapses a structure into its inner type or a `MonadError` error.+-- | Unwraps the success case, or throws the failure mapped by the function. collapse :: (MonadError e m, Bifurcate f)          => (CErr f -> e) -> f -> m (CRes f) collapse toErr = either (throwError . toErr) pure . bifurcate --- | Collapses a structure using an error mapper.+-- | Unwraps the success case, or throws the error info mapped by the function. (??) :: forall a m e. (MonadError e m, Bifurcate a)      => m a -> (CErr a -> e) -> m (CRes a) action ?? toErr = action >>= collapse toErr --- | Collapses a structure using a constant error.+-- | Unwraps the success case, or throws a constant error. Chains to peel nested layers:+-- @m ? e1 ? e2@. (?) :: forall m e a. (MonadError e m, Bifurcate a)     => m a -> e -> m (CRes a) action ? err = action ?? const err --- | Collapses any value based on a predicate+-- | Passes the value on if it satisfies the predicate, else throws the error built from+-- it: @(m ?> p) toErr@. (?>) :: forall m e a. (MonadError e m)      => m a -> (a -> Bool) -> (a -> e) -> m a (?>) action predicate toErr = do   val <- action   if predicate val then pure val else throwError $ toErr val --- | Collapses a structure and recovers to a value dependent on the error+-- | Unwraps the success case, or recovers with a value computed from the error info. (??~) :: forall m a. (Bifurcate a, Monad m) => m a -> (CErr a -> CRes a) -> m (CRes a) action ??~ defaultFunc = action >>= either (pure . defaultFunc) pure . bifurcate --- | Collapses a structure, and recovers to a constant default value in the error case+-- | Unwraps the success case, or recovers with a default value. (?~) :: forall m a. (Bifurcate a, Monad m) => m a -> CRes a -> m (CRes a) action ?~ defaultVal = action ??~ (const defaultVal) --- |  Non-empty is success state. Returns the collection as-is.+-- | Succeeds if non-empty, returning the collection; else throws the constant error. (?+) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> e -> m (t a) (?+) action err = do   xs <- action   if null xs then throwError err else pure xs --- | Single element is success state.  Returns the single element+-- | Succeeds if there is exactly one element, returning it; else throws the error built+-- from the 'CardinalityError'. (?!) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> (CardinalityError (t a) -> e) -> m a (?!) action toErr = do@@ -66,14 +66,14 @@     [x] -> pure x     _   -> throwError $ toErr $ TooMany xs --- | Empty is success state. Returns Unit.+-- | Succeeds if empty, returning @()@; else throws the error built from the collection. (?∅) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> (t a -> e) -> m () (?∅) action toErr = do   xs <- action   if null xs then pure () else throwError (toErr xs) --- | Non-unicode alias for `(?∅)`+-- | ASCII alias for '?∅'. (?@) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> (t a -> e) -> m () (?@) = (?∅)
test/Railroad/MonadErrorSpec.hs view
@@ -4,6 +4,8 @@ module Railroad.MonadErrorSpec where  import           Control.Monad.Except+import           Control.Monad.State   (State, modify, runState)+import           Data.Validation       (Validation (..)) import           Data.Functor.Identity import           Railroad.MonadError import           Test.Hspec@@ -12,6 +14,13 @@ runMonadError :: ExceptT String Identity a -> Either String a runMonadError = runIdentity . runExceptT +-- Like 'runMonadError', and counts how many 'tick's ran.+runCount :: ExceptT String (State Int) a -> (Either String a, Int)+runCount m = runState (runExceptT m) 0++tick :: ExceptT String (State Int) ()+tick = modify (+ 1)+ spec :: Spec spec = do   describe "MonadError version of Operators" $ do@@ -57,3 +66,39 @@           runMonadError (pure [] ?∅ const "not empty") `shouldBe` Right ()         it "fails on non-empty" $ do           runMonadError (pure [1 :: Int] ?∅ const "not empty") `shouldBe` Left "not empty"++    describe "Fallback Operator (?|)" $ do+      it "does not run the right action when the left succeeds" $ do+        runCount (pure (Just 'a') ?| (tick >> pure (Nothing :: Maybe Char)) ? "none")+          `shouldBe` (Right 'a', 0)+      it "runs the right action when the left fails" $ do+        runCount (pure (Nothing :: Maybe Int) ?| (tick >> pure (Just 7)) ? "none")+          `shouldBe` (Right 7, 1)+      it "keeps the last error when everything fails" $ do+        runCount (pure (Left "first" :: Either String Int) ?| pure (Left "second") ?? id)+          `shouldBe` (Left "second", 0)+        runCount (pure (Nothing :: Maybe Int) ?| pure Nothing ? "none")+          `shouldBe` (Left "none", 0)+      it "chains mixed structures, stopping at the first success" $ do+        let chain = pure (Nothing :: Maybe Int) ?| pure (Left "db" :: Either String Int)+                      ?| (tick >> pure (Just 3)) ?| (tick >> tick >> pure (Just 4)) ? "gone"+        runCount chain `shouldBe` (Right 3, 1)++    describe "Accumulating Fallback Operator (?|<>)" $ do+      it "does not run the right action when the left succeeds" $ do+        runCount (pure (Just 'a') ?|<> (tick >> pure (Nothing :: Maybe Char)) ? "none")+          `shouldBe` (Right 'a', 0)+      it "combines the errors in source order when everything fails" $ do+        runCount (pure (Left "A" :: Either String Int) ?|<> pure (Left "B") ?|<> pure (Left "C") ?? id)+          `shouldBe` (Left "ABC", 0)+      it "mixes structures with the same error and success types" $ do+        runCount (pure (Left "A" :: Either String Int) ?|<> pure (Failure "B" :: Validation String Int) ?? id)+          `shouldBe` (Left "AB", 0)+        runCount (pure (Left "A" :: Either String Int) ?|<> (tick >> pure (Success 5 :: Validation String Int)) ?? id)+          `shouldBe` (Right 5, 1)+      it "drops earlier errors once a later source succeeds" $ do+        runCount (pure (Left "A" :: Either String Int) ?|<> pure (Left "B") ?|<> pure (Right 3) ?? id)+          `shouldBe` (Right 3, 0)+      it "needs nothing special for Maybe sources" $ do+        runCount (pure (Nothing :: Maybe Int) ?|<> pure Nothing ? "none")+          `shouldBe` (Left "none", 0)
test/RailroadSpec.hs view
@@ -6,6 +6,7 @@ import           Data.Validation import           Effectful import           Effectful.Error.Dynamic+import           Effectful.State.Static.Local import           Railroad import           Test.Hspec @@ -13,6 +14,13 @@ runRail :: Eff '[Error String] a -> Either String a runRail = runPureEff . runErrorNoCallStack +-- Like 'runRail', and counts how many 'tick's ran.+runCount :: Eff '[Error String, State Int] a -> (Either String a, Int)+runCount = runPureEff . runState 0 . runErrorNoCallStack++tick :: State Int :> es => Eff es ()+tick = modify (+ (1 :: Int))+ spec :: Spec spec = do   describe "Bifurcate Instances" $ do@@ -99,3 +107,46 @@         runRail (pure [] ?∅ const "not empty") `shouldBe` Right ()       it "fails on non-empty" $ do         runRail (pure [1 :: Int] ?∅ const "not empty") `shouldBe` Left "not empty"++  describe "Fallback Operator (?|)" $ do+    it "does not run the right action when the left succeeds" $ do+      runCount (pure (Just 'a') ?| (tick >> pure (Nothing :: Maybe Char)) ? "none")+        `shouldBe` (Right 'a', 0)++    it "runs the right action when the left fails" $ do+      runCount (pure (Nothing :: Maybe Int) ?| (tick >> pure (Just 7)) ? "none")+        `shouldBe` (Right 7, 1)++    it "keeps the last error when everything fails" $ do+      runCount (pure (Left "first" :: Either String Int) ?| pure (Left "second") ?? id)+        `shouldBe` (Left "second", 0)+      runCount (pure (Nothing :: Maybe Int) ?| pure Nothing ? "none")+        `shouldBe` (Left "none", 0)++    it "chains mixed structures, stopping at the first success" $ do+      let chain = pure (Nothing :: Maybe Int) ?| pure (Left "db" :: Either String Int)+                    ?| (tick >> pure (Just 3)) ?| (tick >> tick >> pure (Just 4)) ? "gone"+      runCount chain `shouldBe` (Right 3, 1)++  describe "Accumulating Fallback Operator (?|<>)" $ do+    it "does not run the right action when the left succeeds" $ do+      runCount (pure (Just 'a') ?|<> (tick >> pure (Nothing :: Maybe Char)) ? "none")+        `shouldBe` (Right 'a', 0)++    it "combines the errors in source order when everything fails" $ do+      runCount (pure (Left "A" :: Either String Int) ?|<> pure (Left "B") ?|<> pure (Left "C") ?? id)+        `shouldBe` (Left "ABC", 0)++    it "mixes structures with the same error and success types" $ do+      runCount (pure (Left "A" :: Either String Int) ?|<> pure (Failure "B" :: Validation String Int) ?? id)+        `shouldBe` (Left "AB", 0)+      runCount (pure (Left "A" :: Either String Int) ?|<> (tick >> pure (Success 5 :: Validation String Int)) ?? id)+        `shouldBe` (Right 5, 1)++    it "drops earlier errors once a later source succeeds" $ do+      runCount (pure (Left "A" :: Either String Int) ?|<> pure (Left "B") ?|<> pure (Right 3) ?? id)+        `shouldBe` (Right 3, 0)++    it "needs nothing special for Maybe sources" $ do+      runCount (pure (Nothing :: Maybe Int) ?|<> pure Nothing ? "none")+        `shouldBe` (Left "none", 0)