{-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE DataKinds #-} module Examples where import Control.Monad.Except (MonadError, ExceptT, runExceptT) import Control.Monad.Oops ( CouldBe, Variant ) import Data.Text (Text) import Text.Read (readMaybe) import Data.Functor.Identity (Identity) import Control.Monad.Trans (MonadIO) import Data.Function ((&)) import qualified Control.Monad.Oops as OO import qualified System.IO as IO import Control.Monad.IO.Class (MonadIO(..)) import Control.Exception (IOException) import Control.Monad.Catch (MonadCatch) -- | A simple function that throws an error. -- -- The type is the one that is inferred by GHC. readIntV1 :: () => MonadError (Variant e) m => OO.CouldBeF e Text => String -> m Int readIntV1 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- | A simple function that throws an error. -- -- This is the same as before, but we can rewrite constraint on 'e' differently. readIntV2 :: () => MonadError (Variant e) m => e `CouldBe` Text => String -> m Int readIntV2 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- | A simple function that throws an error. -- -- We can also use 'ExceptT' readIntV3 :: () => e `CouldBe` Text => String -> ExceptT (Variant e) Identity Int readIntV3 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- | A simple IO function that throws an error. -- -- We can also use 'ExceptT' of 'IO'. readIntV4 :: () => e `CouldBe` Text => String -> ExceptT (Variant e) IO Int readIntV4 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- | A simple function that throws an error. -- -- Or use MonadIO instead of IO directly. readIntV5 :: () => MonadError (Variant e) m => MonadIO m => e `CouldBe` Text => String -> m Int readIntV5 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- | A simple function that throws an error. -- -- Or use 'MonadIO' with 'ExceptT'. readIntV6 :: () => MonadIO m => e `CouldBe` Text => String -> ExceptT (Variant e) m Int readIntV6 s = case readMaybe @Int s of Just i -> return i Nothing -> OO.throw @Text "Not an integer" -- We can represent each error as a separate type. data NotAnInteger = NotAnInteger data NotPositive = NotPositive -- | A simple function can throw two errors readPositiveInt1 :: () => MonadIO m => e `CouldBe` NotAnInteger => e `CouldBe` NotPositive => String -> ExceptT (Variant e) m Int readPositiveInt1 s = case readMaybe @Int s of Just i -> if i > 0 then return i else OO.throw NotPositive Nothing -> OO.throw NotAnInteger -- We can call a function that throws an error of type 'Text' and allow the -- error to propagate by declaring we also throw an error of type 'Text'. example1 :: () => e `CouldBe` Text => String -> ExceptT (Variant e) IO Int example1 s = do i <- readInt s liftIO $ IO.print i return i where readInt :: () => e `CouldBe` Text => String -> ExceptT (Variant e) IO Int readInt = error "unimplemented" -- Or alternatively, we can catch the error of type 'Text' and handle it. -- in which case the error doesn't propagate. Notice the 'e CouldBe Text' -- constraint is not needed in this case. example2 :: () => String -> ExceptT (Variant e) IO Int example2 s = do i <- readInt s & OO.catch @Text (\_ -> pure 0) liftIO $ IO.print i return i where readInt :: () => e `CouldBe` Text => String -> ExceptT (Variant e) IO Int readInt = error "unimplemented" -- When we don't throw any errors, we can use the 'runOops' function to -- convert the 'ExceptT' to an 'IO' action. example3 :: () => String -> IO Int example3 s = OO.runOops $ do i <- readInt s & OO.catch @Text (\_ -> pure 0) liftIO $ IO.print i return i where readInt :: () => e `CouldBe` Text => String -> ExceptT (Variant e) IO Int readInt = error "unimplemented" data FileNotFound = FileNotFound data FileNotReadable = FileNotReadable data Errors1 = Errors1NotPositive NotPositive | Errors1NotAnInteger NotAnInteger -- We can call a function that throws multiple errors into a function -- that only throws one by just catching and rethrowing the one -- error. example4 :: () => MonadIO m => e `CouldBe` Errors1 => String -> ExceptT (Variant e) m Int example4 s = do i <- readPositiveInt1 s & OO.catch @NotPositive (OO.throw . Errors1NotPositive) & OO.catch @NotAnInteger (OO.throw . Errors1NotAnInteger) liftIO $ IO.print i return i -------------------------------------------------------------------------------- -- Embedding 'Oops' into vanilla 'ExceptT' and 'Either' code. -- We we have a function that only throws one error, we can use 'runOopsInExceptT' -- to remove the 'Variant' wrapper leaving only the 'ExceptT'. example5 :: () => MonadIO m => String -> ExceptT Errors1 m Int example5 s = OO.runOopsInExceptT $ do i <- readPositiveInt1 s & OO.catch @NotPositive (OO.throw . Errors1NotPositive) & OO.catch @NotAnInteger (OO.throw . Errors1NotAnInteger) liftIO $ IO.print i return i -- We we have a function that only throws one error, we can use 'runOopsInEither' -- to remove the 'Variant' wrapper and the 'Except' leaving only the 'Either'. example6 :: () => MonadIO m => String -> m (Either Errors1 Int) example6 s = OO.runOopsInEither $ do i <- readPositiveInt1 s & OO.catch @NotPositive (OO.throw . Errors1NotPositive) & OO.catch @NotAnInteger (OO.throw . Errors1NotAnInteger) liftIO $ IO.print i return i -------------------------------------------------------------------------------- -- Embedding vanilla 'ExceptT' and 'Either' code into 'Oops' code. -- We can call a function that throws multiple errors into a function -- that only throws one by just catching and rethrowing the one -- error. -- We can call a function that throws vanilla 'ExceptT' errors and use 'onLeft' -- to catch and rethrow the errors as oops errors. example7 :: () => MonadIO m => e `CouldBe` NotPositive => e `CouldBe` NotAnInteger => String -> ExceptT (Variant e) m Int example7 s = do i <- readInt s & OO.onLeft OO.throw pos <- requirePositive i & OO.onLeft OO.throw liftIO $ IO.print pos return pos where readInt :: MonadIO m => String -> m (Either NotAnInteger Int) readInt = error "unimplemented" requirePositive :: MonadIO m => Int -> m (Either NotPositive Int) requirePositive = error "unimplemented" -- 'onLeftThrow' is shorthand for 'onLeft throw'. example8 :: () => MonadIO m => e `CouldBe` NotPositive => e `CouldBe` NotAnInteger => String -> ExceptT (Variant e) m Int example8 s = do i <- readInt s & OO.onLeftThrow pos <- requirePositive i & OO.onLeftThrow liftIO $ IO.print pos return pos where readInt :: MonadIO m => String -> m (Either NotAnInteger Int) readInt = error "unimplemented" requirePositive :: MonadIO m => Int -> m (Either NotPositive Int) requirePositive = error "unimplemented" -- We can similarly call a function that throws via vanilla 'ExceptT' example9 :: () => MonadIO m => e `CouldBe` NotPositive => e `CouldBe` NotAnInteger => String -> ExceptT (Variant e) m Int example9 s = do i <- liftIO (runExceptT (readInt s)) & OO.onLeftThrow pos <- liftIO (runExceptT (requirePositive i)) & OO.onLeftThrow liftIO $ IO.print pos return pos where readInt :: String -> ExceptT NotAnInteger IO Int readInt = error "unimplemented" requirePositive :: Int -> ExceptT NotPositive IO Int requirePositive = error "unimplemented" -- We can similarly call a function that throws via vanilla 'ExceptT' example10 :: () => MonadIO m => e `CouldBe` NotPositive => e `CouldBe` NotAnInteger => String -> ExceptT (Variant e) m Int example10 s = do i <- liftIO (runExceptT (readInt s)) & OO.onLeftThrow pos <- liftIO (runExceptT (requirePositive i)) & OO.onLeftThrow liftIO $ IO.print pos return pos where readInt :: String -> ExceptT NotAnInteger IO Int readInt = error "unimplemented" requirePositive :: Int -> ExceptT NotPositive IO Int requirePositive = error "unimplemented" -- We can also catch runtime exceptions and rethrow them as checked exceptions. example11 :: () => MonadIO m => MonadCatch m => e `CouldBe` IOException => ExceptT (Variant e) m String example11 = do i <- liftIO (IO.readFile "moo") & OO.onException @IOException OO.throw liftIO $ IO.print i return i -- The 'onExceptionThrow' is shorthand for 'onException throw'. example12 :: () => MonadIO m => MonadCatch m => e `CouldBe` IOException => ExceptT (Variant e) m String example12 = do i <- liftIO (IO.readFile "moo") & OO.onExceptionThrow @IOException liftIO $ IO.print i return i