packages feed

oops-examples-0.2.0.0: src/Examples.hs

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