{-# LANGUAGE LambdaCase #-}

import Control.Monad.Except (MonadIO(..), MonadError(..), runExceptT)
import Control.Monad.State.Lazy (StateT(runStateT), MonadState(put, get), MonadIO(..))
import Control.Monad ( void )

data Errors = Quit | Show

-- A little demo of a program with multiple effects: exceptions, state and IO
-- The programs keeps requesting the user to enter a string; these are then 
-- collected in the internal store. Exception throwing and catching is used for 
-- escaping from the loop
errIOStateDemo :: (MonadError Errors m, MonadIO m, MonadState [String] m) => m ()
errIOStateDemo = do
    -- This is a computation w.r.t. the monad "m", which is a polymorphic parameter

    -- liftIO coerces the IO monad to m, using the type constraint "MonadIO m"
    liftIO $ putStr "Enter a line: "

    line <- liftIO getLine
    case line of

         -- throw an exception if the user entered "q" = user is willing to quit 
         "q" -> throwError Quit

         -- throw an exception if the user entered "s" = user is willing to see the state
         "s" -> do lines <- get; liftIO $ print lines; throwError Show

         -- proceed with the control flow normally, otherwise
         _   -> return ()

    -- Grab the current state (thanks to "MonadState [String] m")
    state <- get

    -- Update the state (thanks to "MonadState [String] m") 
    put $ line : state

    -- recursive call
    errIOStateDemo

    -- exception handling clause  
    `catchError` \case

         -- return back to the main loop
         Show -> errIOStateDemo

         -- reraise the exception
         e    -> throwError e

-- In order to use errIOStateDemo, we need to build a concrete monad, so
-- that the interpreter knows how to instantiate 'm'. From the following 
-- code the interpreter will deduce that m = StateT [String] (ExceptT Errors IO) 
-- i.e. m is the state transformer of the exception transformer of IO
main :: IO ()
main = void (runExceptT (runStateT errIOStateDemo []))
