railroad 0.1.2.0 → 0.1.2.1
raw patch · 5 files changed
+54/−5 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- railroad.cabal +1/−1
- src/Railroad.hs +6/−4
- test/Railroad/MonadErrorSpec.hs +18/−0
- test/RailroadSpec.hs +23/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for railroad +## 0.1.2.1 -- 2026-10-11+* Fix overlapping instances: `Either e (Maybe a)`, `Maybe (Maybe a)`, `Either e Bool` and the like+ did not compile when used with `?`, so nested layers could not be peeled (the README's first+ example did not compile). The outermost layer is now peeled first, as documented; lists and other+ traversables inside a layer behave as before. Tests added, including the README example.+ ## 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
railroad.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: railroad-version: 0.1.2.0+version: 0.1.2.1 license: BSD-3-Clause license-file: LICENSE author: Frederik Kallstrup Mastratisi
src/Railroad.hs view
@@ -53,18 +53,20 @@ Success a -> Right a -- validation :: (e -> b) -> (a -> b) -> Validation e a -> b -instance (Traversable t, CErr (t Bool) ~ (), CRes (t Bool) ~ t ())+-- The traversable instances overlap the base ones for e.g. @Either e (Maybe a)@. They are+-- INCOHERENT so the base instance wins and the outer layer is peeled first (@m ? e1 ? e2@).+instance {-# INCOHERENT #-} (Traversable t, CErr (t Bool) ~ (), CRes (t Bool) ~ t ()) => Bifurcate (t Bool) where bifurcate = bifurcate . sequenceA . fmap bifurcate -- Bool is not Applicative, so we need to map to Either first-instance (Traversable t, CErr (t (Maybe a)) ~ (), CRes (t (Maybe a)) ~ t a)+instance {-# INCOHERENT #-} (Traversable t, CErr (t (Maybe a)) ~ (), CRes (t (Maybe a)) ~ t a) => Bifurcate (t (Maybe a)) where bifurcate = bifurcate . sequenceA-instance (Traversable t, CErr (t (Either e a)) ~ e, CRes (t (Either e a)) ~ t a)+instance {-# INCOHERENT #-} (Traversable t, CErr (t (Either e a)) ~ e, CRes (t (Either e a)) ~ t a) => Bifurcate (t (Either e a)) where bifurcate = sequenceA -- pedagogically: bifurcate = bifurcate . sequenceA-instance (Traversable t, Semigroup e, CErr (t (Validation e a)) ~ e, CRes (t (Validation e a)) ~ t a)+instance {-# INCOHERENT #-} (Traversable t, Semigroup e, CErr (t (Validation e a)) ~ e, CRes (t (Validation e a)) ~ t a) => Bifurcate (t (Validation e a)) where bifurcate = bifurcate . sequenceA
test/Railroad/MonadErrorSpec.hs view
@@ -102,3 +102,21 @@ it "needs nothing special for Maybe sources" $ do runCount (pure (Nothing :: Maybe Int) ?|<> pure Nothing ? "none") `shouldBe` (Left "none", 0)++ describe "Layered structures (the outermost layer is peeled first)" $ do+ it "peels Either of Maybe one layer at a time" $ do+ runMonadError (pure (Right (Just 1) :: Either String (Maybe Int)) ? "outer" ? "inner") `shouldBe` Right 1+ runMonadError (pure (Right Nothing :: Either String (Maybe Int)) ? "outer" ? "inner") `shouldBe` Left "inner"+ runMonadError (pure (Left "x" :: Either String (Maybe Int)) ? "outer" ? "inner") `shouldBe` Left "outer"+ it "peels Maybe of Maybe and Either of Bool" $ do+ runMonadError (pure (Just Nothing :: Maybe (Maybe Int)) ? "o" ? "i") `shouldBe` Left "i"+ runMonadError (pure (Right False :: Either String Bool) ? "db" ? "denied") `shouldBe` Left "denied"+ it "runs the README example" $ do+ let readmeExample :: Either String Int+ readmeExample = runExcept $ do+ x <- pure (Just 2) ? "Value missing"+ y <- pure (Right $ Just 1) ? "Outer fail" ? "Inner fail"+ z <- pure [Just 4] ? "List failed" ?! const "Not a single element"+ q <- pure Nothing ?~ 1+ pure (x + y + z + q)+ readmeExample `shouldBe` Right 8
test/RailroadSpec.hs view
@@ -150,3 +150,26 @@ it "needs nothing special for Maybe sources" $ do runCount (pure (Nothing :: Maybe Int) ?|<> pure Nothing ? "none") `shouldBe` (Left "none", 0)++ describe "Layered structures (the outermost layer is peeled first)" $ do+ it "peels Either of Maybe one layer at a time" $ do+ let ok = pure (Right (Just 1)) :: Eff '[Error String] (Either String (Maybe Int))+ runRail (ok ? "outer" ? "inner") `shouldBe` Right 1+ runRail (pure (Right Nothing :: Either String (Maybe Int)) ? "outer" ? "inner") `shouldBe` Left "inner"+ runRail (pure (Left "x" :: Either String (Maybe Int)) ? "outer" ? "inner") `shouldBe` Left "outer"++ it "peels Maybe of Maybe" $ do+ runRail (pure (Just (Just 1) :: Maybe (Maybe Int)) ? "o" ? "i") `shouldBe` Right 1+ runRail (pure (Just Nothing :: Maybe (Maybe Int)) ? "o" ? "i") `shouldBe` Left "i"+ runRail (pure (Nothing :: Maybe (Maybe Int)) ? "o" ? "i") `shouldBe` Left "o"++ it "peels Either of Bool" $ do+ runRail (pure (Right True :: Either String Bool) ? "db" ? "denied") `shouldBe` Right ()+ runRail (pure (Right False :: Either String Bool) ? "db" ? "denied") `shouldBe` Left "denied"+ runRail (pure (Left "x" :: Either String Bool) ? "db" ? "denied") `shouldBe` Left "db"++ it "peels Either of Either" $ do+ runRail (pure (Right (Left "in") :: Either String (Either String Int)) ? "out" ?? id) `shouldBe` Left "in"++ it "still traverses a list inside a layer" $ do+ runRail (pure (Just [Just 1, Just 2 :: Maybe Int]) ? "o" ? "i") `shouldBe` Right [1, 2]