railroad 0.2.0.0 → 0.2.0.1
raw patch · 8 files changed
+56/−1 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +5/−0
- railroad.cabal +1/−1
- readme.md +31/−0
- src/Railroad.hs +1/−0
- src/Railroad/Bifurcate.hs +3/−0
- src/Railroad/MonadError.hs +1/−0
- test/Railroad/MonadErrorSpec.hs +7/−0
- test/RailroadSpec.hs +7/−0
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Revision history for railroad +## 0.2.0.1 -- 2026-10-11+* Documentation: a README section on the empty-collection gotcha (`?` on a traversable checks that+ every element succeeds, so an empty one passes; chain `?+` before it), with the quantifiers each+ operator corresponds to, and a note in the haddocks of `Bifurcate` and `?+`. A test pins it.+ ## 0.2.0.0 -- 2026-10-11 * Breaking: `(?>)` no longer throws. `m ?> p` tags the value (`Right a` if `p a`, else `Left a`, carrying the rejected value) and is finished with `?` or `??`: `(m ?> p) toErr` becomes
railroad.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: railroad-version: 0.2.0.0+version: 0.2.0.1 license: BSD-3-Clause license-file: LICENSE author: Frederik Kallstrup Mastratisi
readme.md view
@@ -180,7 +180,9 @@ | `Validation e a` | `Failure` | `e` | `a` | | `t f` | Any element is error | `CErr f` | `t (CRes f)` | +An empty `t f` has no element to fail, so it succeeds: see [the gotcha](#gotcha-an-empty-collection-passes). + For the latter each operator (`?+`, `?!`, `?∅`) interpret what cardinalities of a Foldable counts as errors differently. To wit: @@ -248,6 +250,35 @@ 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.++## Gotcha: an empty collection passes++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:++```haskell+_ <- runQuery (permitted uid asset) ? err500 ?+ err403 ? err403+-- ^ db error ^ >= 1 row ^ all True+```++The cardinality operators are the other quantifiers, so the usual+conditions are each one 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) | ## More See [`railroad.md`](railroad.md) for a deeper tour, or just read the
src/Railroad.hs view
@@ -35,6 +35,7 @@ -- | Succeeds if non-empty, returning the collection; else throws the constant error.+-- Chain it before @?@ on a traversable, which an empty collection passes. (?+) :: forall es e t a. (Error e :> es, Foldable t) => Eff es (t a) -> e -> Eff es (t a) (?+) action err = do
src/Railroad/Bifurcate.hs view
@@ -37,6 +37,9 @@ CRes (t a) = t (CRes a) -- | A catamorphism to Either+--+-- A traversable fails at its first failing element; an empty one succeeds (use @?+@ to+-- also require an element). class Bifurcate f where bifurcate :: f -> Either (CErr f) (CRes f) instance Bifurcate Bool where
src/Railroad/MonadError.hs view
@@ -33,6 +33,7 @@ action ? err = action ?? const err -- | Succeeds if non-empty, returning the collection; else throws the constant error.+-- Chain it before @?@ on a traversable, which an empty collection passes. (?+) :: forall m e t a. (MonadError e m, Foldable t) => m (t a) -> e -> m (t a) (?+) action err = do
test/Railroad/MonadErrorSpec.hs view
@@ -36,6 +36,13 @@ in (runMonadError (pure xs ?∅ (\ys -> "got " ++ show (length ys))) === model) .&&. (runMonadError (pure xs ?@ (\ys -> "got " ++ show (length ys))) === model) + describe "an empty collection passes ? (vacuous truth); ?+ requires an element" $ do+ it "[] passes ? on [Bool]" $ do+ runMonadError (pure ([] :: [Bool]) ? "denied") `shouldBe` Right []+ it "?+ then ? needs at least one element, all succeeding" $ property $ \(bs :: [Bool]) ->+ runMonadError (pure bs ?+ "none" ? "denied")+ === (if null bs then Left "none" else if and bs then Right (map (const ()) bs) else Left "denied")+ describe "fixity (all operators are infixl 1, like >>=, <&> and &)" $ do it "composes with <&> without parentheses" $ do runMonadError (pure (Right 2 :: Either String Int) ? "e" <&> (+ 1)) `shouldBe` Right 3
test/RailroadSpec.hs view
@@ -35,6 +35,13 @@ in (runRail (pure xs ?∅ (\ys -> "got " ++ show (length ys))) === model) .&&. (runRail (pure xs ?@ (\ys -> "got " ++ show (length ys))) === model) + describe "an empty collection passes ? (vacuous truth); ?+ requires an element" $ do+ it "[] passes ? on [Bool]" $ do+ runRail (pure ([] :: [Bool]) ? "denied") `shouldBe` Right []+ it "?+ then ? needs at least one element, all succeeding" $ property $ \(bs :: [Bool]) ->+ runRail (pure bs ?+ "none" ? "denied")+ === (if null bs then Left "none" else if and bs then Right (map (const ()) bs) else Left "denied")+ describe "fixity (all operators are infixl 1, like >>=, <&> and &)" $ do it "composes with <&> without parentheses" $ do runRail (pure (Right 2 :: Either String Int) ? "e" <&> (+ 1)) `shouldBe` Right 3