packages feed

railroad 0.2.1.0 → 0.3.0.0

raw patch · 10 files changed

+547/−251 lines, 10 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Railroad.Cardinality: IsEmpty :: CardinalityError ta
- Railroad.Cardinality: TooMany :: ta -> CardinalityError ta
- Railroad.Cardinality: cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
- Railroad.Cardinality: data CardinalityError ta
+ Railroad: (?∃) :: forall (es :: [Effect]) e t a. (Error e :> es, Foldable t, Bifurcate a) => Eff es (t a) -> ([CErr a] -> e) -> Eff es [CRes a]
+ Railroad.Bifurcate: IsEmpty :: CardinalityError ta
+ Railroad.Bifurcate: TooMany :: ta -> CardinalityError ta
+ Railroad.Bifurcate: anySucceeds :: (Foldable t, Bifurcate a) => t a -> Either [CErr a] [CRes a]
+ Railroad.Bifurcate: atLeastOne :: Foldable t => t a -> Maybe (t a)
+ Railroad.Bifurcate: cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
+ Railroad.Bifurcate: data CardinalityError ta
+ Railroad.Bifurcate: exactlyOne :: Foldable t => t a -> Either (CardinalityError (t a)) a
+ Railroad.Bifurcate: failures :: (Foldable t, Bifurcate a) => t a -> [CErr a]
+ Railroad.Bifurcate: instance GHC.Classes.Eq ta => GHC.Classes.Eq (Railroad.Bifurcate.CardinalityError ta)
+ Railroad.Bifurcate: instance GHC.Internal.Show.Show ta => GHC.Internal.Show.Show (Railroad.Bifurcate.CardinalityError ta)
+ Railroad.Bifurcate: neg :: Bifurcate a => a -> Either (CRes a) (CErr a)
+ Railroad.Bifurcate: noneAtAll :: Foldable t => t a -> Either (t a) ()
+ Railroad.Bifurcate: partitionBifurcate :: (Foldable t, Bifurcate a) => t a -> ([CErr a], [CRes a])
+ Railroad.Bifurcate: successes :: (Foldable t, Bifurcate a) => t a -> [CRes a]
+ Railroad.Bifurcate: tagBy :: (a -> Bool) -> a -> Either a a
+ Railroad.MonadError: (?∃) :: forall m e t a. (MonadError e m, Foldable t, Bifurcate a) => m (t a) -> ([CErr a] -> e) -> m [CRes a]
- Railroad: infixl 1 ?@
+ Railroad: infixl 1 ?∃
- Railroad.MonadError: infixl 1 ?@
+ Railroad.MonadError: infixl 1 ?∃

Files

CHANGELOG.md view
@@ -1,5 +1,21 @@ # Revision history for railroad +## 0.3.0.0 -- unreleased+* The test suite and CI now run on GHC 9.10, 9.12 and 9.14 (`tested-with`). The dependency bounds are+  unchanged, so older GHCs are still allowed but are no longer tested.+* Breaking: `Railroad.Cardinality` is merged into `Railroad.Bifurcate`. `CardinalityError` and+  `cardinalityErr` are exported from `Railroad.Bifurcate`, and still from `Railroad` and+  `Railroad.MonadError`.+* New classifiers, each a plain function into `Either`: `neg` (swap failure and success),+  `tagBy` (the classifier behind `?>`), `atLeastOne`, `exactlyOne` and `noneAtAll` (behind `?+`,+  `?!` and `?∅`), `partitionBifurcate`, `successes`, `failures` and `anySucceeds`. Any verb applies to a classified value:+  `xs <&> exactlyOne ??~ f`, `xs <&> anySucceeds ?<* h ? e`.+* New operator `(?∃)`: succeeds if at least one element succeeds, returning the successes, and+  otherwise throws an error built from every failure. `xs ?∃ f` is `xs <&> anySucceeds ?? f`.+* `(?>)`, `(?+)`, `(?!)` and `(?∅)` are now defined over the classifiers. Their types and behaviour+  are unchanged.+* `CardinalityError` derives `Eq` and `Show`.+ ## 0.2.1.0 -- 2026-10-11 * Add the tap operator `(?<*)`: runs an effect on the failure and passes the original value through   unchanged, so a failure can be logged or counted before a constant error throws it away:
railroad.cabal view
@@ -1,22 +1,22 @@ cabal-version:      3.0 name:               railroad-version:            0.2.1.0+version:            0.3.0.0 license:            BSD-3-Clause license-file:       LICENSE author:             Frederik Kallstrup Mastratisi maintainer:         mastratisi@proton.me category:           Control, Error Handling build-type:         Simple-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+tested-with:        GHC == 9.10.3, GHC == 9.12.4, GHC == 9.14.1 extra-doc-files:    CHANGELOG.md, readme.md-synopsis:           Railway-oriented error handling: unwrap or throw in one short operator.+synopsis:           Railway-oriented error handling: unwrap, throw, recover or fall back description:     Railway-oriented error handling for Haskell: one short operator per     step, and the failure paths stop taking up lines. -    Each operator unwraps one layer of a result (@Maybe@, @Either@, @Bool@,-    @Validation@, or a collection of them) and says what to throw if it-    failed. Where you would write+    Each operator looks at a result (@Maybe@, @Either@, @Bool@,+    @Validation@, or a collection of them), decides whether it is a failure,+    and says what to do about it. Where you would write      > rows <- runQuery (userById uid) >>= either (const (throwError err503)) pure     > user <- case rows of@@ -31,30 +31,35 @@     Layers are unwrapped left to right: @? err503@ unwraps the database     @Either@, then @?!@ demands exactly one row (none is a 404, several a 500). -    * Works in any @MonadError@ ("Railroad.MonadError": @mtl@, @ExceptT@,-      servant's @Handler@) and in @effectful@ with the @Error@ effect-      ("Railroad").+    What to do about a failure: -    * Unwrap or throw: @?@ (constant error) and @??@ (error computed from-      the failure). Recover instead of throwing: @?~@ and @??~@.+    * Throw: @?@ (constant error) and @??@ (error computed from the failure). -    * Tag by a predicate with @?>@, or fall back to another source with @?|@-      and @?|<>@.+    * Recover: @?~@ and @??~@. -    * Log a failure before a constant error discards it, with @?\<*@:-      @runQuery q ?\<* logDbError ? err503@.+    * Observe, then carry on: @?\<*@, as in @runQuery q ?\<* logDbError ? err503@. -    * @Validation@ failures accumulate: validate a whole request and report-      every problem at once.+    * Fall back to another source: @?|@, or @?|<>@ to combine the errors. -    * All operators are @infixl 1@ like @>>=@ and @\<&\>@, so they chain without-      parentheses.+    What counts as a failure: -    Collections are checked with quantifiers: @?@ is ∀ ("every element-    succeeds", so an empty collection passes), @?+@ is ∃ ("at least one"),-    @?!@ is ∃! ("exactly one") and @?∅@ is ¬∃ ("none"). They chain:-    @xs ?+ e1 ? e2@ means "at least one, and all succeed". The readme has the-    details and a longer servant example.+    * @Nothing@, @Left@, @False@ and @Failure@. A collection fails if any+      element does, so an empty collection passes. @Validation@ failures+      accumulate, so a whole request can report every problem at once.++    * Collections are also checked by size: @?+@ (at least one), @?!@ (exactly+      one), @?∅@ (none), and @?∃@ (at least one element succeeded; keeps the+      successes).++    * Classifiers: @?>@ tags by a predicate. Any function into @Either@,+      @Maybe@, @Bool@ or @Validation@ is a classifier, applied with @\<&\>@.+      "Railroad.Bifurcate" provides @neg@, @atLeastOne@, @exactlyOne@,+      @noneAtAll@ and @anySucceeds@.++    Works in any @MonadError@ ("Railroad.MonadError": @mtl@, @ExceptT@,+    servant's @Handler@) and in @effectful@ with the @Error@ effect+    ("Railroad"). All operators are @infixl 1@ like @>>=@ and @\<&\>@, so+    they chain without parentheses. The readme has more examples. homepage:     https://github.com/mastratisi/railroad bug-reports:  https://github.com/mastratisi/railroad/issues source-repository head@@ -73,7 +78,6 @@     import:           stuff     exposed-modules:  Railroad                       Railroad.Bifurcate-                      Railroad.Cardinality                       Railroad.MonadError     hs-source-dirs:   src     default-language: GHC2021
readme.md view
@@ -1,7 +1,10 @@ # railroad -**A terse railway-oriented error handling DSL for Haskell. Unwrap `Maybe`, `Either`, `Bool` or `Validation`, or throw a mapped error, with one short operator.**+**Railway-oriented error handling for Haskell. Unwrap a `Maybe`, `Either`, `Bool` or `Validation`, or throw a mapped error, with one short operator.** +[![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)+ Instead of  ```haskell@@ -18,15 +21,12 @@ user <- runQuery (userById uid) ? err503 ?! cardinalityErr err404 (const err500) ``` -Here `runQuery` returns `Eff es (Either DbError [row])`. Each operator-unwraps 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.+`runQuery` returns `Eff es (Either DbError [row])`. Each operator unwraps one+layer, left to right: `? err503` unwraps the database `Either`, then `?!`+demands exactly one row (none is a 404, several a 500). -The purpose of `railroad` is to error handle tersely, keeping the-happy-path clean of control-flow and functor unpacking.  It does this-with 11 combinators, that abstract over binary co-products (`Bool`,-`Maybe`, `Either`, `Validation`) and Traversables or Folds over them.+It works in any `MonadError` (`mtl`, `ExceptT`, servant's `Handler`) and in+`effectful` with the `Error` effect.  ```haskell import Railroad.MonadError@@ -34,63 +34,76 @@  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 +  x <- pure (Just 2) ? "Value missing"                          -- Just 2        ~> 2+  y <- pure (Right (Just 1)) ? "Outer fail" ? "Inner fail"      -- Right (Just 1) ~> Just 1 ~> 1+  z <- pure [Just 4] ? "List failed" ?! const "Not one element" -- [Just 4]      ~> [4] ~> 4+  q <- pure Nothing ?~ 1                                        -- 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`.+## How it works -[![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)+Every operator does two things: it decides whether a value is a failure, then it says what+to do about a failure. Operators read left to right, and each one unwraps one layer: ----+```+runQuery (userById uid)           Eff es (Either DbError [User])+  ? err503                        Left _  → throw err503          Right rows → rows :: [User]+  ?! cardinalityErr err404 (const err500)+                                  [] → throw err404, several → throw err500+                                  [u] → u :: User+``` -## Modules+What counts as a failure depends on the type: `Nothing`, `Left`, `False` or `Failure`. A+list fails if any element does. There are four things to do about a failure: -- `Railroad`: the operators for the `effectful` `Error` effect.-- `Railroad.MonadError`: the same operators for any `MonadError` (`mtl`, `ExceptT`, servant's `Handler`).-- `Railroad.Bifurcate`: the `Bifurcate` class and the operators that never throw-  (`?>`, `?~`, `??~`, `?|`, `?|<>`, `?<*`); `Railroad.Cardinality`: `CardinalityError`.-  Both of the above re-export these, so you rarely import them directly.+| Do | Operators | Example |+|---|---|---|+| throw an error | `?` `??` | `action ? err503` |+| recover with a default | `?~` `??~` | `action ?~ 0` |+| run an effect, then carry on | `?<*` | `action ?<* logIt ? err503` |+| try another source | `?\|` `?\|<>` | `cache k ?\| db k ? NotFound` | +The operators for collections (`?+`, `?!`, `?∅`, `?∃`) decide by how many elements there+are or succeeded; the next sections go through them. The operators also read as logical+connectives and quantifiers (∧, ∨, ¬, ∀, ∃); that reading is in [`railroad.md`](railroad.md).++## Contents++- [How it works](#how-it-works)+- [Install](#install)+- [Example: a servant backend](#example-a-servant-backend)+- [The operators](#the-operators)+- [What counts as an error](#what-counts-as-an-error)+- [Gotcha: an empty collection passes](#gotcha-an-empty-collection-passes)+- [Behavior under an evolving code base](#behavior-under-an-evolving-code-base)+- [Exercises](#exercises)+- [Modules](#modules)+ ## Install +### Hackage++The package is called `railroad`: https://hackage.haskell.org/package/railroad+ ### Nix -Here is an expression you can add to `ghcWithPackages`:+An expression for `ghcWithPackages`:+ ```nix-railroad = pkgs.haskellPackages.callCabal2nix "railroad" (pkgs.fetchFromGitHub +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   +  ; sha256 = pkgs.lib.fakeSha256; # Nix prints the real hash on the 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`:+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])`.  ```haskell getUser :: Error ServerError :> es => UserId -> Eff es UserDTO@@ -105,14 +118,12 @@  updateAsset :: Error ServerError :> es => UserId -> Asset -> Eff es () updateAsset uid asset = do-  -- permitted is an SQL exists, so it always returns exactly one row (see the gotcha below)+  -- `permitted` is an SQL exists: one row, so no empty-collection gotcha (see below)   _ <- runQuery (permitted uid asset) ? err500 ? err403   -- db error, then "not permitted"   ... ``` -Each operator unwraps 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:+The same handlers in plain Haskell:  ```haskell getUser' uid = do@@ -134,172 +145,251 @@   ... ``` -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`            |-| `?>`     | Tag by predicate (`Left` carries the value)   | `action ?> isGood ? NotGood`  |-| `??~`    | 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`         |-| `?<*`    | Run an effect on a failure, then carry on     | `action ?<* logIt ? MyError`  |---All operators are `infixl 1`, like `>>=`, `<&>` and `&`, so they chain-left to right with them without parentheses:-`runQuery q ? err503 <&> toDTO`.--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?-+## The operators -### 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.+| Role | Operator | What it does | Example |+|---|---|---|---|+| Throw | `?` | Unwrap, or throw a constant | `action ? MyError` |+| | `??` | Unwrap, or throw an error computed from the failure | `action ?? toMyError` |+| Collections | `?+` | Throw on an empty collection | `items ?+ NoResults` |+| | `?!` | Throw unless there is exactly one element | `items ?! fromCardinalityErr` |+| | `?∅` (alias `?@`) | Throw on a non-empty collection | `items ?∅ DuplicateFound` |+| | `?∃` | Throw unless at least one element succeeded; keep the successes | `sources ?∃ toErr` |+| Recover | `?~` | Recover with a constant | `action ?~ defaultVal` |+| | `??~` | Recover with a value computed from the failure | `action ??~ toDefault` |+| Classify | `?>` | Tag by a predicate: a rejected value becomes the failure | `action ?> isGood ? NotGood` |+| Observe | `?<*` | Run an effect on a failure, then carry on | `action ?<* logIt ? MyError` |+| Fall back | `?\|` | Try the next source on failure; keep the last error | `cache k ?\| db k ? NotFound` |+| | `?\|<>` | The same, combining all the errors with `<>` | `a ?\|<> b ?? toErr` | -For the former, they all (`?`, `??`, `?~`, `??~`) interpret what-an error is the same way:+All operators are `infixl 1`, like `>>=`, `<&>` and `&`, so they chain left to+right with them without parentheses: `runQuery q ? err503 <&> toDTO`. `<$>` binds+tighter (infixl 4), so `f <$> action ? err` applies `f` to the wrapped value before+it is unwrapped, and `f` receives the `Either`. Map the result with `<&>` after the+operator. - | 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)`         |+### Classifiers -An empty `t f` has no element to fail, so it succeeds: see [the gotcha](#gotcha-an-empty-collection-passes).+A classifier is any function whose result type the operators already understand:+`Either e r`, `Maybe r`, `Bool`, `Validation e r`, or a traversable of those. It+sorts a value into failure or success and says nothing about what to do with a+failure. In `Either e r` the failure carries the reason (`e`) and the success carries+the value you continue with (`r`). A `Maybe` loses the reason+and a `Bool` loses the value. +Existing functions are classifiers: `lookup k`, `readMaybe`, or a parser such as+`parseAge :: String -> Either String Int`. Apply one with `<&>` and finish with an+operator, which decides what happens on failure: -For the latter each operator (`?+`, `?!`, `?∅`) interpret what-cardinalities of a Foldable counts as errors differently. To wit:+```haskell+readBody <&> parseAge ?? BadRequest            -- throw, built from the parse error+readBody <&> parseAge ??~ const 18             -- recover with a default+readBody <&> parseAge ?<* logIt ? BadAge       -- log the parse error, then throw+``` -| Operator    | Success Condition   | Error Info               | Result Info |-|:------------|:--------------------|:-------------------------|:------------|-| `?+`        | Non-empty           | `()`                     | `t a`       |-| `?!`        | Exactly one element | `CardinalityError (t a)` | `a`         |-| `?∅` / `?@` | Is empty            | `t a`                    | `()`        |+The classifiers in the library are pure functions in `Railroad.Bifurcate` and need no+monad: -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+neg         :: Bifurcate a => a -> Either (CRes a) (CErr a)   -- swap success and failure+tagBy       :: (a -> Bool) -> a -> Either a a                 -- Right when the predicate holds+atLeastOne  :: Foldable t => t a -> Maybe (t a)+exactlyOne  :: Foldable t => t a -> Either (CardinalityError (t a)) a+noneAtAll   :: Foldable t => t a -> Either (t a) ()+successes   :: (Foldable t, Bifurcate a) => t a -> [CRes a]+failures    :: (Foldable t, Bifurcate a) => t a -> [CErr a]+anySucceeds :: (Foldable t, Bifurcate a) => t a -> Either [CErr a] [CRes a] ``` -### What happens in the error case?-When an error is detected, the railroad "derails":+Any classifier combines with any of four verbs. Only the throw column has dedicated+operators (`m` is the action, `n` another source, `e` a constant error, `f` a mapper,+`d` a default, `h` an effect): -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.+| Classifier | throw | recover | observe | fall back |+|---|---|---|---|---|+| structural (∀ on a collection) | `m ? e` `m ?? f` | `m ?~ d` `m ??~ f` | `m ?<* h` | `m ?\| n` |+| `tagBy` | `m ?> p ? e` | `m ?> p ?~ d` | `m ?> p ?<* h` | `m ?> p ?\| n` |+| `atLeastOne` | `m ?+ e` | `m <&> atLeastOne ?~ d` | `m <&> atLeastOne ?<* h` | `m <&> atLeastOne ?\| n` |+| `exactlyOne` | `m ?! f` | `m <&> exactlyOne ??~ f` | `m <&> exactlyOne ?<* h` | `m <&> exactlyOne ?\| n` |+| `noneAtAll` | `m ?∅ f` | `m <&> noneAtAll ??~ f` | `m <&> noneAtAll ?<* h` | `m <&> noneAtAll ?\| n` |+| `anySucceeds` | `m ?∃ f` | `m <&> anySucceeds ??~ f` | `m <&> anySucceeds ?<* h` | `m <&> anySucceeds ?\| n` |+| `neg` | `m <&> neg ? e` | `m <&> neg ??~ f` | `m <&> neg ?<* h` | `m <&> neg ?\| n` | -**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.+A fall-back chain is finished with `?` or `??` to turn a total failure into a throw. A+failure with several cases is `m ?? \case ...`. Nothing covers a success that also+carries errors (`These`). +`neg` expresses "must not exist" on a single value: `findByEmail e <&> neg ? err409`+throws `err409` when a user is found and passes on `Nothing`. `?∃` is "at least one+of these sources answered": it throws when all of them failed and returns the+successes otherwise. -**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.+### Fallbacks -### 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:+`?|` and `?|<>` try one source after another and stop at the first success. The+sources may be different structures (a cache returning `Maybe`, a database+returning `Either`) with the same success type. The right-hand side only runs if+the left one failed. 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:+`?|<>` needs the sources to share an error type and combines all the errors with+`<>`, in source order. Map differing error types to a common one first:  ```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.+`Validation` runs all independent checks and collects every failure. A fallback+chain succeeds on the first good answer.  ### Observing failures-A constant error such as `? err503` throws the underlying failure away.-`?<*` runs an effect on the failure first and passes the value through-unchanged, so the next operator still sees it: +`? err503` throws the underlying failure away. `?<*` runs an effect on the failure+first and passes the value through unchanged, so the next operator still sees it:+ ```haskell runQuery q ?<* logDbError ? err503 ``` -The handler is a function of the failure (`CErr`), returns `m ()`, and-runs only when there is a failure. Like `<*` it keeps the left value, and-like the other operators in `Railroad.Bifurcate` it never throws.+The handler takes the failure (`CErr`), returns `m ()`, and runs only on a+failure. It never throws. +## What counts as an error++The operators `?`, `??`, `?~`, `??~`, `?>`, `?|` and `?<*` agree on what a failure+is, through the `Bifurcate` class:++| Type | Failure | Failure info (`CErr`) | Success info (`CRes`) |+|---|---|---|---|+| `Bool` | `False` | `()` | `()` |+| `Maybe a` | `Nothing` | `()` | `a` |+| `Either e a` | `Left` | `e` | `a` |+| `Validation e a` | `Failure` | `e` | `a` |+| `t f` | any element fails | `CErr f` | `t (CRes f)` |++On a traversable of `Bool`, `Maybe` or `Either` the check stops at the first+failure. On `t (Validation e a)` the `Semigroup` on `e` collects all of them, so+`pure (map validateFile files) ?? badRequest` turns every invalid file into one 400.+An empty `t f` has no element to fail, so it succeeds (see the gotcha below).++The collection operators `?+`, `?!` and `?∅` look at the number of elements:++| Operator | Succeeds when | Failure info | Success info |+|:---|:---|:---|:---|+| `?+` | non-empty | `()` | `t a` |+| `?!` | exactly one element | `CardinalityError (t a)` | `a` |+| `?∅` | empty | `t a` | `()` |++```haskell+data CardinalityError ta = IsEmpty | TooMany ta++cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e   -- a fold for it+```++When an operator throws, the error mapping is applied to the failure info, the+result is thrown, and the monadic sequence stops. `?~` and `??~` never throw:+they put a default value on the happy path instead.+ ## Gotcha: an empty collection passes -On a traversable, `?` and `??` check that **every** element succeeds-(∀). Over an empty collection that holds vacuously, like `and []`:+On a traversable, `?` and `??` check that **every** element succeeds (∀). Over+an empty collection that holds vacuously, like `and []`:  ```haskell pure ([] :: [Bool]) ? err403    -- succeeds, with [] ``` -That surprises a permission check where "no rows" should mean "not-permitted". Require at least one element first, with `?+` (∃), and-chain the two checks:+In a permission check, "no rows" should mean "not permitted", so require at least+one element first, with `?+`:  ```haskell _ <- runQuery (permitted uid asset) ? err500 ?+ err403 ? err403---                                    ^ db error ^ >= 1 row ^ all True+--                                  ^ db error ^ >= 1 row ^ all True ``` -The cardinality operators are the other quantifiers, so the usual-conditions are each one operator, and they chain:+Each quantifier has an operator, and they chain: -| Operator                            | Succeeds when                       | Logic                           |-|-------------------------------------|-------------------------------------|---------------------------------|-| `?` / `??` on `[Bool]`, `[Maybe a]` | every element succeeds              | ∀ x ∈ xs. succeeds x            |-| `?+`                                | there is at least one element       | ∃ x ∈ xs                        |-| `?!`                                | there is exactly one element        | ∃! x ∈ xs                       |-| `?∅`                                | there is no element                 | ¬∃ x ∈ xs                       |-| `?+` then `?`                       | there is one, and every one succeeds | (∃ x ∈ xs) ∧ (∀ x ∈ xs. succeeds x) |+| Operator | Succeeds when | Logic |+|---|---|---|+| `?` / `??` on `[Bool]`, `[Maybe a]` | every element succeeds | ∀ x ∈ xs. succeeds x |+| `?+` | there is at least one element | ∃ x ∈ xs |+| `?!` | there is exactly one element | ∃! x ∈ xs |+| `?∅` | there is no element | ¬∃ x ∈ xs |+| `?∃` | at least one element succeeds | ∃ x ∈ xs. succeeds x |+| `?+` then `?` | there is one, and every one succeeds | (∃ x ∈ xs) ∧ (∀ x ∈ xs. succeeds x) | -## 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.+## Behavior under an evolving code base +The same operator works on `Maybe a`, `Either e a` and a list of either, so a+dependency can change from `Maybe` to `Either` without touching its callers.+[`examples/error-handling-styles`](examples/error-handling-styles) writes eleven+handlers in three styles (railroad, plain `case`, and `orThrow`/`expectOne`+helpers) and applies nine changes to each. Cells are edit sites / lines touched.++| Change | railroad | helpers | `case` |+|---|---|---|---|+| `Maybe a` to `Either CacheError a` | 0 / 0 | 2 / 6 | 2 / 8 |+| `Maybe a` to `[Maybe a]` (shards, first hit wins) | 3 / 5 | 3 / 6 | 3 / 5 |+| `Either DbError [User]` to `Either DbError (Maybe User)` | 1 / 2 | 4 / 18 | 3 / 14 |+| `Either DbError [Bool]` to `Either DbError Bool` (`SELECT EXISTS`) | 3 / 6 | 3 / 6 | 6 / 15 |+| `validateName`: `Validation` to `Either` | 1 / 2 | 1 / 2 | 1 / 2 |+| database timeout becomes 504 | 4 / 10 | 9 / 19 | 8 / 16 |+| several grants per user: any granting row suffices | 3 / 6 | 4 / 7 | 3 / 12 |+| audit-log every duplicate registration | 1 / 2 | 1 / 2 | 1 / 2 |+| add a third fallback source | 1 / 1 | 1 / 6 | 2 / 11 |+| **all nine** | **17 / 34** | **28 / 72** | **29 / 85** |++Adding a source to a fallback chain is one line:++```haskell+-- before+cache k ?| db k ? NotFound+-- after+cache k ?| db k ?| legacy k ? NotFound+```++In the `case` version it is another level of nesting around the existing branches.+The timeout change took four edits, one per `? Unavailable`, because the error is a+literal at each call site; with `?? dbErr` from the start it is one.++One change went wrong silently. `validateName` changed from `Validation` to+`Either`, and `pure (map validateName names) ?? BadRequest` kept compiling but+reported the first problem instead of all of them. `?` over a collection chooses+between stopping at the first failure and collecting every one by the element+type. The other two styles stopped compiling. After changing a return type, check+the operators at its call sites: a list where there was one value changes what `?`+means, and an empty list passes (see the gotcha above).++One LLM wrote the handlers. The test code base has eleven handlers+with nine changes done to it and only GHC 9.10.3 was used.++## Exercises++[`examples/exercises`](examples/exercises) teaches the operators by translation.+Each exercise is a function written with `case` and an empty stub. You fill in the+stub with the operators until a test that runs both on random inputs agrees. There+are 38 exercises in 13 lessons, with hints and an answer key.++```+cd examples/exercises && cabal test exercises+```++## Modules++- `Railroad`: the operators for the `effectful` `Error` effect.+- `Railroad.MonadError`: the same operators for any `MonadError`.+- `Railroad.Bifurcate`: everything that does not throw. The `Bifurcate` class, the classifiers+  (`neg`, `tagBy`, `successes`, `failures`, `anySucceeds`, `atLeastOne`, `exactlyOne`,+  `noneAtAll`), `CardinalityError`, and the operators `?>`, `?~`, `??~`, `?|`, `?|<>`, `?<*`.++The first two re-export it, so you rarely import it directly. The reasoning behind+the design is in [`railroad.md`](railroad.md).+ ## 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++DM me on https://x.com/mastratisi97 . I like to hear if other people find this useful.
src/Railroad.hs view
@@ -7,16 +7,14 @@ -- @Error@ effect, keeping the happy path clean. For @MonadError@ use "Railroad.MonadError". module Railroad   ( module Railroad.Bifurcate-  , module Railroad.Cardinality   , collapse-  , (??), (?), (?+), (?!), (?∅), (?@)+  , (??), (?), (?+), (?!), (?∅), (?@), (?∃)   ) where -import           Data.Foldable           (toList)+import           Data.Functor            ((<&>)) import           Effectful               (Eff, type (:>)) import           Effectful.Error.Dynamic (Error, throwError_) import           Railroad.Bifurcate-import           Railroad.Cardinality   -- | Unwraps the success case, or throws the failure mapped by the function.@@ -36,35 +34,37 @@  -- | Succeeds if non-empty, returning the collection; else throws the constant error. -- Chain it before @?@ on a traversable, which an empty collection passes.+-- Sugar for @xs <&> 'atLeastOne' ? err@. (?+) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> e -> Eff es (t a)-(?+) action err = do-  xs <- action-  if null xs then throwError_ err else pure xs+action ?+ err = action <&> atLeastOne ? err   -- | Succeeds if there is exactly one element, returning it; else throws the error built--- from the 'CardinalityError'.+-- from the 'CardinalityError'. Sugar for @xs <&> 'exactlyOne' ?? toErr@. (?!) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> (CardinalityError (t a) -> e) -> Eff es a-(?!) action toErr = do-  xs <- action-  case toList xs of-    []  -> throwError_ $ toErr IsEmpty-    [x] -> pure x-    _   -> throwError_ $ toErr $ TooMany xs+action ?! toErr = action <&> exactlyOne ?? toErr   -- | Succeeds if empty, returning @()@; else throws the error built from the collection.+-- Sugar for @xs <&> 'noneAtAll' ?? toErr@. (?∅) :: 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)+action ?∅ toErr = action <&> noneAtAll ?? toErr  -- | ASCII alias for '?∅'. (?@) :: forall es e t a. (Error e :> es, Foldable t)      => Eff es (t a) -> ((t a) -> e) -> Eff es () (?@) = (?∅) -infixl 1 ??, ?, ?!, ?+, ?∅, ?@+-- | Succeeds if at least one element succeeds, returning the successes; else throws the+-- error built from every failure (none if the collection was empty). Unlike '?+' it asks+-- whether an element succeeded, not whether there is one. On a nested base type such as+-- @Maybe (Maybe a)@ the outer layer is the collection and the inner one is checked.+-- Sugar for @xs <&> 'anySucceeds' ?? toErr@.+(?∃) :: forall es e t a. (Error e :> es, Foldable t, Bifurcate a)+     => Eff es (t a) -> ([CErr a] -> e) -> Eff es [CRes a]+action ?∃ toErr = action <&> anySucceeds ?? toErr++infixl 1 ??, ?, ?!, ?+, ?∅, ?@, ?∃
src/Railroad/Bifurcate.hs view
@@ -6,19 +6,37 @@  -- | -- The 'Bifurcate' class, which splits a structure (@Bool@, @Maybe@, @Either@, @Validation@,--- traversables of them) into a failure and a success case, and the operators that never--- throw. The throwing operators are in "Railroad" and "Railroad.MonadError".+-- traversables of them) into a failure and a success case, the classifiers built on it, and+-- the operators that never throw. Nothing here depends on how a failure is thrown; the+-- throwing operators are in "Railroad" (@effectful@) and "Railroad.MonadError" (@mtl@). module Railroad.Bifurcate-  ( Bifurcate (..), CErr, CRes+  ( -- * The class+    Bifurcate (..), CErr, CRes+    -- * Classifiers+    -- $classifiers+  , neg, tagBy, partitionBifurcate, successes, failures, anySucceeds+    -- * Cardinality+  , CardinalityError (..), cardinalityErr, atLeastOne, exactlyOne, noneAtAll+    -- * Operators that never throw+    -- $operators   , (?>), (??~), (?~), (?|), (?|<>), (?<*)   ) where  import           Data.Bifunctor  (first) import           Data.Bool       (bool)+import           Data.Either     (partitionEithers)+import           Data.Foldable   (toList) import           Data.Kind       (Type) import           Data.Validation (Validation (..)) +-- $classifiers+-- A classifier is a plain function into 'Either'. It needs no functor and never throws:+-- apply it with @\<&\>@ and finish with any operator, e.g. @xs \<&\> 'exactlyOne' ?? f@. +-- $operators+-- These work in any 'Functor' or 'Monad' and never throw. Finish a chain with @?@ or @??@+-- from "Railroad" or "Railroad.MonadError" to turn a remaining failure into a throw.+ -- | 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@@ -75,10 +93,74 @@   bifurcate = bifurcate . sequenceA  +-- | Negation: swaps failure and success. A verb after it acts on a value that was meant to+-- fail, and the error mapper sees what succeeded instead.+--+-- > lookupUser email <&> neg ?? const err409     -- Nothing passes, Just _ is a 409+--+-- On a traversable the failure is "some element failed", so @neg@ is the negation of the+-- all-succeed reading: @xs <&> neg ? e@ passes exactly when @xs ? e@ would throw.+-- @neg . atLeastOne@ is @noneAtAll@, and @neg (neg x)@ is @'bifurcate' x@.+neg :: Bifurcate a => a -> Either (CRes a) (CErr a)+neg = either Right Left . bifurcate++-- | The classifier behind @(?>)@: @Right@ if the value satisfies the predicate, else @Left@+-- carrying it.+tagBy :: (a -> Bool) -> a -> Either a a+tagBy predicate x = if predicate x then Right x else Left x++-- | Splits a collection of results into its failures and its successes, in order.+partitionBifurcate :: (Foldable t, Bifurcate a) => t a -> ([CErr a], [CRes a])+partitionBifurcate = partitionEithers . map bifurcate . toList++-- | The success values of a collection of results; the failures are dropped.+successes :: (Foldable t, Bifurcate a) => t a -> [CRes a]+successes = snd . partitionBifurcate++-- | The failure values of a collection of results.+failures :: (Foldable t, Bifurcate a) => t a -> [CErr a]+failures = fst . partitionBifurcate++-- | Classifies a collection of results as having at least one success. The success carries+-- every success value; the failure carries every failure value (none if the collection+-- was empty): @xs <&> anySucceeds ?? f@.+anySucceeds :: (Foldable t, Bifurcate a) => t a -> Either [CErr a] [CRes a]+anySucceeds xs = case partitionBifurcate xs of+  (es, [])  -> Left es+  (_, rs)   -> Right rs++-- | Why a collection was not a single element: empty, or too many (carrying it).+data CardinalityError ta = IsEmpty | TooMany ta+  deriving (Eq, Show)++-- | Fold for 'CardinalityError': one result per case.+cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e+cardinalityErr onEmpty onTooMany = \case+  IsEmpty -> onEmpty+  TooMany xs -> onTooMany xs++-- | Classifies a collection as non-empty. The failure carries nothing, so the result is+-- @Bifurcate@ with @()@ as the error info: @xs <&> atLeastOne ? e@ is @xs ?+ e@.+atLeastOne :: Foldable t => t a -> Maybe (t a)+atLeastOne xs = if null xs then Nothing else Just xs++-- | Classifies a collection as having exactly one element, returning it. The failure says+-- whether there were none or too many: @xs <&> exactlyOne ?? f@ is @xs ?! f@.+exactlyOne :: Foldable t => t a -> Either (CardinalityError (t a)) a+exactlyOne xs = case toList xs of+  []  -> Left IsEmpty+  [x] -> Right x+  _   -> Left (TooMany xs)++-- | Classifies a collection as empty. The failure carries the collection:+-- @xs <&> noneAtAll ?? f@ is @xs ?∅ f@.+noneAtAll :: Foldable t => t a -> Either (t a) ()+noneAtAll xs = if null xs then Right () else Left xs+ -- | Tags the value: @Right@ if it satisfies the predicate, else @Left@ carrying it. Finish -- with @?@ or @??@ (the mapper sees the rejected value): @m ?> p ? err@. (?>) :: forall m a. Functor m => m a -> (a -> Bool) -> m (Either a a)-action ?> predicate = (\x -> if predicate x then Right x else Left x) <$> action+action ?> predicate = tagBy predicate <$> action  -- | Unwraps the success case, or recovers with a value computed from the error info. (??~) :: forall m a. (Functor m, Bifurcate a) => m a -> (CErr a -> CRes a) -> m (CRes a)
− src/Railroad/Cardinality.hs
@@ -1,17 +0,0 @@-{-# LANGUAGE LambdaCase #-}---- |--- The error info of the cardinality operators (@?!@ in "Railroad" and "Railroad.MonadError"):--- why a collection was not a single element.-module Railroad.Cardinality-  ( CardinalityError (..), cardinalityErr-  ) where---- | Why a collection was not a single element: empty, or too many (carrying it).-data CardinalityError ta = IsEmpty | TooMany ta---- | Fold for 'CardinalityError': one result per case.-cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e-cardinalityErr onEmpty onTooMany = \case-  IsEmpty -> onEmpty-  TooMany xs -> onTooMany xs
src/Railroad/MonadError.hs view
@@ -6,15 +6,13 @@  module Railroad.MonadError   ( module Railroad.Bifurcate-  , module Railroad.Cardinality   , collapse-  , (??), (?), (?+), (?!), (?∅), (?@)+  , (??), (?), (?+), (?!), (?∅), (?@), (?∃)   ) where  import           Control.Monad.Except (MonadError (..))-import           Data.Foldable        (toList)+import           Data.Functor         ((<&>)) import           Railroad.Bifurcate-import           Railroad.Cardinality  -- | Unwraps the success case, or throws the failure mapped by the function. collapse :: (MonadError e m, Bifurcate f)@@ -34,33 +32,35 @@  -- | Succeeds if non-empty, returning the collection; else throws the constant error. -- Chain it before @?@ on a traversable, which an empty collection passes.+-- Sugar for @xs <&> 'atLeastOne' ? err@. (?+) :: 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+action ?+ err = action <&> atLeastOne ? err  -- | Succeeds if there is exactly one element, returning it; else throws the error built--- from the 'CardinalityError'.+-- from the 'CardinalityError'. Sugar for @xs <&> 'exactlyOne' ?? toErr@. (?!) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> (CardinalityError (t a) -> e) -> m a-(?!) action toErr = do-  xs <- action-  case toList xs of-    []  -> throwError $ toErr IsEmpty-    [x] -> pure x-    _   -> throwError $ toErr $ TooMany xs+action ?! toErr = action <&> exactlyOne ?? toErr  -- | Succeeds if empty, returning @()@; else throws the error built from the collection.+-- Sugar for @xs <&> 'noneAtAll' ?? toErr@. (?∅) :: 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)+action ?∅ toErr = action <&> noneAtAll ?? toErr  -- | ASCII alias for '?∅'. (?@) :: forall m e t a. (MonadError e m, Foldable t)      => m (t a) -> (t a -> e) -> m () (?@) = (?∅) -infixl 1 ??, ?, ?!, ?+, ?∅, ?@+-- | Succeeds if at least one element succeeds, returning the successes; else throws the+-- error built from every failure (none if the collection was empty). Unlike '?+' it asks+-- whether an element succeeded, not whether there is one. On a nested base type such as+-- @Maybe (Maybe a)@ the outer layer is the collection and the inner one is checked.+-- Sugar for @xs <&> 'anySucceeds' ?? toErr@.+(?∃) :: forall m e t a. (MonadError e m, Foldable t, Bifurcate a)+     => m (t a) -> ([CErr a] -> e) -> m [CRes a]+action ?∃ toErr = action <&> anySucceeds ?? toErr++infixl 1 ??, ?, ?!, ?+, ?∅, ?@, ?∃
test/Railroad/BifurcateSpec.hs view
@@ -4,7 +4,7 @@  import           Control.Monad.State    (modify, runState) import           Data.Bifunctor         (first)-import           Data.Either            (isLeft)+import           Data.Either            (isLeft, isRight) import           Data.Functor.Identity import           Data.Maybe             (listToMaybe) import           Data.Proxy             (Proxy (..))@@ -81,3 +81,92 @@     it "[Either], [Either]" $ law (Proxy @[Either [Int] Int]) (Proxy @[Either [Int] Int])     it "is associative" $ property $ \(a :: Either [Int] Int) (b :: Either [Int] Int) (c :: Either [Int] Int) ->       runIdentity ((pure a ?|<> pure b) ?|<> pure c) === runIdentity (pure a ?|<> (pure b ?|<> pure c))++  describe "tagBy" $ do+    it "keeps the value on both sides" $ property $ \(p :: Fun Int Bool) (x :: Int) ->+      either id id (tagBy (applyFun p) x) === x+    it "is Right exactly when the predicate holds" $ property $ \(p :: Fun Int Bool) (x :: Int) ->+      isRight (tagBy (applyFun p) x) === applyFun p x++  describe "partitionBifurcate, successes, failures" $ do+    let laws :: forall a. (Shape a, Bifurcate [a], CErr [a] ~ CErr a, CRes [a] ~ [CRes a])+             => Proxy a -> Property+        laws _ = property $ \(xs :: [a]) (ys :: [a]) ->+          let (es, rs) = partitionBifurcate xs+          in (length es + length rs === length xs)+             .&&. (partitionBifurcate (xs ++ ys) === (es <> failures ys, rs <> successes ys))+             .&&. (isRight (bifurcate xs) === null es)+             .&&. (either (const True) (== rs) (bifurcate xs))+             .&&. (successes xs === rs)+             .&&. (failures xs === es)+        single :: forall a. Shape a => Proxy a -> Property+        single _ = property $ \(x :: a) ->+          (successes [x] === either (const []) pure (bifurcate x))+          .&&. (failures [x] === either pure (const []) (bifurcate x))+    it "Bool"       $ laws (Proxy @Bool)+    it "Maybe"      $ laws (Proxy @(Maybe Int))+    it "Either"     $ laws (Proxy @(Either Int Int))+    it "Validation" $ laws (Proxy @(Validation [Int] Int))+    forShapes "a singleton splits as its element does" single++  describe "anySucceeds" $ do+    let law :: forall a. (Shape a) => Proxy a -> Property+        law _ = property $ \(xs :: [a]) ->+          let r = anySucceeds xs+          in (isRight r === any (isRight . bifurcate) xs)+             .&&. (r === (if any (isRight . bifurcate) xs then Right (successes xs) else Left (failures xs)))+             .&&. (either length (const 0) r === (if any (isRight . bifurcate) xs then 0 else length xs))+    it "Bool"       $ law (Proxy @Bool)+    it "Maybe"      $ law (Proxy @(Maybe Int))+    it "Either"     $ law (Proxy @(Either Int Int))+    it "Validation" $ law (Proxy @(Validation [Int] Int))+    it "fails on an empty collection with no failures" $+      anySucceeds ([] :: [Maybe Int]) `shouldBe` Left []++  describe "neg" $ do+    it "Bool and Maybe swap to the unit failure" $ do+      neg True `shouldBe` Left ()+      neg False `shouldBe` Right ()+      neg (Just 'x') `shouldBe` Left 'x'+      neg (Nothing :: Maybe Char) `shouldBe` Right ()+    forShapes "swaps failure and success" $ \(_ :: Proxy a) -> property $+      \(x :: a) -> neg x === either Right Left (bifurcate x)+    forShapes "is an involution: neg (neg x) = bifurcate x" $ \(_ :: Proxy a) -> property $+      \(x :: a) -> neg (neg x) === bifurcate x+    it "of a traversable passes exactly when some element failed" $ property $+      \(xs :: [Either Int Int]) -> isRight (neg xs) === any (isLeft . bifurcate) xs+    it "carries the first failure of a traversable" $ property $ \(xs :: [Either Int Int]) ->+      neg xs === (case [e | Left e <- xs] of { e : _ -> Right e; [] -> Left [a | Right a <- xs] })+    it "of atLeastOne is noneAtAll" $ property $ \(xs :: [Int]) ->+      neg (atLeastOne xs) === noneAtAll xs+    it "turns anySucceeds into all-failed" $ property $ \(xs :: [Either Int Int]) ->+      isRight (neg (anySucceeds xs)) === all (isLeft . bifurcate) xs++  describe "atLeastOne" $ do+    it "classifies a list by emptiness, keeping the list" $ property $ \(xs :: [Int]) ->+      atLeastOne xs === (if null xs then Nothing else Just xs)+    it "works on any Foldable" $ do+      atLeastOne (Just 'a') `shouldBe` Just (Just 'a')+      atLeastOne (Nothing :: Maybe Char) `shouldBe` Nothing++  describe "exactlyOne" $ do+    it "returns the element of a singleton, else says why not" $ property $ \(xs :: [Int]) ->+      exactlyOne xs === (case xs of { [] -> Left IsEmpty; [x] -> Right x; _ -> Left (TooMany xs) })+    it "works on any Foldable" $ do+      exactlyOne (Just 'a') `shouldBe` Right 'a'+      exactlyOne (Nothing :: Maybe Char) `shouldBe` Left IsEmpty++  describe "noneAtAll" $ do+    it "succeeds on an empty collection, else returns it as the failure" $ property $ \(xs :: [Int]) ->+      noneAtAll xs === (if null xs then Right () else Left xs)++  describe "the three agree with each other" $ do+    it "exactlyOne reports IsEmpty exactly when noneAtAll succeeds" $ property $ \(xs :: [Int]) ->+      (exactlyOne xs == Left IsEmpty) === (noneAtAll xs == Right ())+    it "a list of one element satisfies atLeastOne and exactlyOne but not noneAtAll" $ property $ \(x :: Int) ->+      (atLeastOne [x], exactlyOne [x], noneAtAll [x]) === (Just [x], Right x, Left [x])++  describe "CardinalityError" $ do+    it "cardinalityErr is the eliminator" $ property $ \(n :: Int) (xs :: [Int]) ->+      (cardinalityErr n length (IsEmpty :: CardinalityError [Int]) === n)+      .&&. (cardinalityErr n length (TooMany xs) === length xs)
test/Railroad/MonadErrorSpec.hs view
@@ -6,6 +6,7 @@ import           Control.Monad.Except import           Control.Monad.State   (modify, runState) import           Data.Bifunctor        (first)+import           Data.Either           (isRight) import           Data.Functor          ((<&>)) import           Data.Functor.Identity import           Data.Proxy            (Proxy (..))@@ -61,6 +62,21 @@       run (pure (Right 4 :: Either Int Int) ?<* (\e -> modify (e :)) ? "failed") `shouldBe` (Right 4, [])     it "gives the rejected value of ?> to the error mapper" $ do       runMonadError (pure (4 :: Int) ?> (> 5) ?? \n -> "rejected " ++ show n) `shouldBe` Left "rejected 4"++  describe "?∃ (at least one success)" $ do+    it "returns the successes, or throws every failure" $ property $ \(xs :: [Either Int Int]) ->+      runMonadError (pure xs ?∃ show)+        === (if any isRight xs then Right [a | Right a <- xs] else Left (show [e | Left e <- xs]))+    it "throws on an empty collection, with no failures" $+      runMonadError (pure ([] :: [Maybe Int]) ?∃ show) `shouldBe` Left "[]"+    it "passes the successes of a Maybe collection and throws unit failures" $ do+      runMonadError (pure [Just 1, Nothing, Just (3 :: Int)] ?∃ show) `shouldBe` Right [1, 3]+      runMonadError (pure [Nothing, Nothing :: Maybe Int] ?∃ show) `shouldBe` Left "[(),()]"+    it "throws where ?+ passes: every element failed" $ do+      runMonadError (pure [Left 1, Left 2 :: Either Int Int] ?+ "none") `shouldSatisfy` isRight+      runMonadError (pure [Left 1, Left 2 :: Either Int Int] ?∃ show) `shouldBe` Left "[1,2]"+    it "chains left to right with the other operators" $ do+      runMonadError (pure (Right [Just 1, Nothing] :: Either String [Maybe Int]) ? "db" ?∃ show) `shouldBe` Right [1]    describe "README" $ do     it "runs the first example" $ do
test/RailroadSpec.hs view
@@ -3,6 +3,7 @@ module RailroadSpec where  import           Data.Bifunctor          (first)+import           Data.Either             (isRight) import           Data.Functor            ((<&>)) import           Data.Proxy              (Proxy (..)) import           Effectful@@ -61,3 +62,18 @@       run (pure (Right 4 :: Either Int Int) ?<* (\e -> modify (e :)) ? "failed") `shouldBe` (Right 4, [])     it "gives the rejected value of ?> to the error mapper" $ do       runRail (pure (4 :: Int) ?> (> 5) ?? \n -> "rejected " ++ show n) `shouldBe` Left "rejected 4"++  describe "?∃ (at least one success)" $ do+    it "returns the successes, or throws every failure" $ property $ \(xs :: [Either Int Int]) ->+      runRail (pure xs ?∃ show)+        === (if any isRight xs then Right [a | Right a <- xs] else Left (show [e | Left e <- xs]))+    it "throws on an empty collection, with no failures" $+      runRail (pure ([] :: [Maybe Int]) ?∃ show) `shouldBe` Left "[]"+    it "passes the successes of a Maybe collection and throws unit failures" $ do+      runRail (pure [Just 1, Nothing, Just (3 :: Int)] ?∃ show) `shouldBe` Right [1, 3]+      runRail (pure [Nothing, Nothing :: Maybe Int] ?∃ show) `shouldBe` Left "[(),()]"+    it "throws where ?+ passes: every element failed" $ do+      runRail (pure [Left 1, Left 2 :: Either Int Int] ?+ "none") `shouldSatisfy` isRight+      runRail (pure [Left 1, Left 2 :: Either Int Int] ?∃ show) `shouldBe` Left "[1,2]"+    it "chains left to right with the other operators" $ do+      runRail (pure (Right [Just 1, Nothing] :: Either String [Maybe Int]) ? "db" ?∃ show) `shouldBe` Right [1]