packages feed

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 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