railroad 0.1.0.0 → 0.1.0.1
raw patch · 4 files changed
+140/−2 lines, 4 filesPVP: minor bump suggested
API additions: PVP suggests at least a minor version bump
API changes (from Hackage documentation)
+ Railroad.MonadError: (?!) :: forall m e t a. (MonadError e m, Foldable t) => m (t a) -> (CardinalityError (t a) -> e) -> m a
+ Railroad.MonadError: (?) :: forall m e a. (MonadError e m, Bifurcate a) => m a -> e -> m (CRes a)
+ Railroad.MonadError: (?+) :: forall m e t a. (MonadError e m, Foldable t) => m (t a) -> e -> m (t a)
+ Railroad.MonadError: (?>) :: forall m e a. MonadError e m => m a -> (a -> Bool) -> (a -> e) -> m a
+ Railroad.MonadError: (??) :: forall a m e. (MonadError e m, Bifurcate a) => m a -> (CErr a -> e) -> m (CRes a)
+ Railroad.MonadError: (??~) :: forall m a. (Bifurcate a, Monad m) => m a -> (CErr a -> CRes a) -> m (CRes a)
+ Railroad.MonadError: (?~) :: forall m a. (Bifurcate a, Monad m) => m a -> CRes a -> m (CRes a)
+ Railroad.MonadError: (?∅) :: forall m e t a. (MonadError e m, Foldable t) => m (t a) -> (t a -> e) -> m ()
+ Railroad.MonadError: IsEmpty :: CardinalityError ta
+ Railroad.MonadError: TooMany :: !ta -> CardinalityError ta
+ Railroad.MonadError: bifurcate :: Bifurcate f => f -> Either (CErr f) (CRes f)
+ Railroad.MonadError: cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
+ Railroad.MonadError: class Bifurcate f
+ Railroad.MonadError: collapse :: (MonadError e m, Bifurcate f) => (CErr f -> e) -> f -> m (CRes f)
+ Railroad.MonadError: data CardinalityError ta
+ Railroad.MonadError: infixl 0 ?∅
+ Railroad.MonadError: infixl 1 ?>
+ Railroad.MonadError: type family CRes f
Files
- CHANGELOG.md +6/−0
- railroad.cabal +3/−2
- src/Railroad/MonadError.hs +71/−0
- test/Railroad/MonadErrorSpec.hs +60/−0
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for railroad +## 0.1.0.1 -- 2026-02-26++* Fix that `Railroad.MonadError` is not exposed in the cabal file+* Fix ambiguous occurrence of `(??~)` in `Railroad.MonadError` by hiding it in import.+* Add tests for the MonadError version of the operators such that issues like the above will be caught + ## 0.1.0.0 -- 2026-02-25 * First version. Released on an unsuspecting world.
railroad.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: railroad-version: 0.1.0.0+version: 0.1.0.1 license: BSD-3-Clause license-file: LICENSE author: Frederik Kallstrup Mastratisi@@ -29,7 +29,7 @@ library import: stuff exposed-modules: Railroad- + Railroad.MonadError hs-source-dirs: src default-language: GHC2021 @@ -41,6 +41,7 @@ hs-source-dirs: test main-is: Main.hs other-modules: RailroadSpec+ Railroad.MonadErrorSpec build-depends: base ^>=4.20.2.0, hspec >= 2.11.14 && < 2.12,
+ src/Railroad/MonadError.hs view
@@ -0,0 +1,71 @@+-- |+-- 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".+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}++module Railroad.MonadError+ ( module Railroad -- re-exports CErr, CRes, Bifurcate, CardinalityError, etc.+ , collapse+ , (??), (?), (?>), (??~), (?~), (?+), (?!), (?∅)+ ) where++import Railroad hiding (collapse, (?!), (?), (?+), (?>),+ (??), (??~), (?~), (?∅))++import Control.Monad.Except (MonadError (..))+import Data.Foldable (toList)++-- | Collapses a structure into its inner type or a `MonadError` error.+collapse :: (MonadError e m, Bifurcate f)+ => (CErr f -> e) -> f -> m (CRes f)+collapse toErr = either (throwError . toErr) pure . bifurcate++(??) :: forall a m e. (MonadError e m, Bifurcate a)+ => m a -> (CErr a -> e) -> m (CRes a)+action ?? toErr = action >>= collapse toErr++(?) :: forall m e a. (MonadError e m, Bifurcate a)+ => m a -> e -> m (CRes a)+action ? err = action ?? const err++(?>) :: 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+(??~) :: 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+(?~) :: forall m a. (Bifurcate a, Monad m) => m a -> CRes a -> m (CRes a)+action ?~ defaultVal = action ??~ (const defaultVal)++(?+) :: 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++(?!) :: 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++(?∅) :: 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)++infixl 0 ??, ?, ?~, ?!, ?+, ?∅+infixl 1 ?>
+ test/Railroad/MonadErrorSpec.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}++module Railroad.MonadErrorSpec where++import Control.Monad.Except+import Data.Functor.Identity+import Data.Validation+import Railroad.MonadError+import Test.Hspec++-- Helper to run MonadError computations in pure Either+runMonadError :: ExceptT String Identity a -> Either String a+runMonadError = runIdentity . runExceptT++spec :: Spec+spec = do+ describe "MonadError version of Operators" $ do+ describe "Basic Operators (? and ??)" $ do+ it "unwraps success values with (?)" $ do+ runMonadError (pure (Just 10 :: Maybe Int) ? "missing") `shouldBe` Right 10+ it "throws constant error on failure with (?)" $ do+ runMonadError (pure (Nothing :: Maybe ()) ? "missing") `shouldBe` Left "missing"+ it "maps internal errors with (??)" $ do+ let action = pure (Left "original" :: Either String String)+ runMonadError (action ?? reverse) `shouldBe` Left "lanigiro"++ describe "Predicate Operator (?>)" $ do+ it "passes when predicate is met" $ do+ runMonadError (pure 10 ?> (> 5) $ const "too small") `shouldBe` Right 10+ it "fails when predicate is not met" $ do+ runMonadError (pure 4 ?> (> 5) $ const "too small") `shouldBe` Left "too small"++ describe "Recovery Operators (?~ and ??~)" $ do+ it "recovers to a constant value with (?~)" $ do+ runMonadError (pure Nothing ?~ 0) `shouldBe` Right (0 :: Int)+ it "recovers using a function with (??~)" $ do+ runMonadError (pure (Left "err") ??~ length) `shouldBe` Right 3++ describe "Cardinality Operators" $ do+ describe "(?+)" $ do+ it "succeeds on non-empty list" $ do+ runMonadError (pure [1, 2, 3] ?+ "empty") `shouldBe` Right [1, 2, 3]+ it "fails on empty list" $ do+ runMonadError (pure ([] :: [Int]) ?+ "empty") `shouldBe` Left "empty"++ describe "(?!)" $ do+ let toErr = cardinalityErr "none" (const "too many")+ it "extracts the single element" $ do+ runMonadError (pure [42] ?! toErr) `shouldBe` Right 42+ it "fails on empty" $ do+ runMonadError (pure ([] :: [Int]) ?! toErr) `shouldBe` Left "none"+ it "fails on multiple elements" $ do+ runMonadError (pure [1, 2] ?! toErr) `shouldBe` Left "too many"++ describe "(?∅)" $ do+ it "succeeds on empty" $ do+ runMonadError (pure [] ?∅ const "not empty") `shouldBe` Right ()+ it "fails on non-empty" $ do+ runMonadError (pure [1] ?∅ const "not empty") `shouldBe` Left "not empty"