{-# LANGUAGE TypeFamilies, ScopedTypeVariables, FlexibleContexts, Rank2Types, ConstraintKinds #-} {-# LANGUAGE MultiParamTypeClasses, NoMonomorphismRestriction #-} {-# LANGUAGE FlexibleInstances, UndecidableInstances, DataKinds, TypeOperators #-} {-# LANGUAGE GADTs, DeriveGeneric #-} {-# LANGUAGE InstanceSigs #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} -- | This effect allows you to "throw" a signal. For the most part signals are the same as checked -- exceptions. The difference here is that the handler has the option to provide the value that -- will be the result /of calling the 'signal' function/. This effectively allows you to have -- recoverable exceptions at the throw site, instead of just at the handling site. module Control.Effects.Signal ( ResumeOrBreak(..), Signal(..), throwSignal, handleSignal , Throw, handleException, handleToEither, module Control.Effects , module Control.Monad.Trans.Except, MaybeT(..), discardAllExceptions, showAllExceptions , HandleException(..), handleWithoutDiscarding, handleToEitherRecursive, SomeSignal , signal ) where import Import hiding (liftThrough) import Control.Monad.Trans.Except import qualified GHC.TypeLits as TL import GHC.TypeLits (TypeError, ErrorMessage(..)) import Control.Effects import Control.Monad.Runnable import GHC.Generics newtype Signal a b m = SignalMethods { _signal :: a -> m b } deriving (Generic) instance Effect (Signal a b) where signal :: forall a b m. MonadEffect (Signal a b) m => a -> m b SignalMethods signal = effect newtype SomeSignal = SomeSignal { getSomeSignal :: Text } deriving (Eq, Ord, Read, Show) type family UnhandledError a b :: ErrorMessage where UnhandledError a Void = 'TL.Text "Unhandled exception of type " ':<>: 'ShowType a ':$$: 'TL.Text "You need to handle all the exceptions before running the computation" UnhandledError a b = 'TL.Text "Unhandled signal of type " ':<>: 'ShowType a ':<>: 'TL.Text " expecting a return value of type " ':<>: 'ShowType b ':$$: 'TL.Text "You need to handle all the signals before running the computation" instance {-# OVERLAPPABLE #-} Monad m => MonadEffect (Signal e b) (ExceptT e m) where effect = SignalMethods throwE instance (Show e, Monad m) => MonadEffect (Signal e b) (ExceptT SomeSignal m) where effect = SignalMethods (throwE . SomeSignal . pack . show) instance Monad m => MonadEffect (Signal a b) (MaybeT m) where effect = SignalMethods (const mzero) instance TypeError (UnhandledError a b) => MonadEffect (Signal a b) IO where effect = undefined instance {-# INCOHERENT #-} (Monad m, b ~ c) => MonadEffect (Signal a c) (RuntimeImplemented (Signal a b) m) where effect = mergeContext $ RuntimeImplemented (liftThrough <$> ask) type Throw e = Signal e Void -- | The handle function will return a value of this type. data ResumeOrBreak b c = Resume b -- ^ Give a value to the caller of 'signal' and keep going. | Break c -- ^ Continue the execution after the handler. The handler will return this value -- | Throw a signal with no possible recovery. The handler is forced to only return the 'Break' -- constructor because it cannot construct a 'Void' value. -- -- If this function is used along with 'handleAsException', this module behaves like regular -- checked exceptions. throwSignal :: MonadEffect (Throw a) m => a -> m b throwSignal = fmap absurd . signal resumeOrBreak :: (b -> a) -> (c -> a) -> ResumeOrBreak b c -> a resumeOrBreak ba _ (Resume b) = ba b resumeOrBreak _ ca (Break c) = ca c collapseEither :: Either a a -> a collapseEither (Left a) = a collapseEither (Right a) = a -- | Handle signals of a computation. The handler function has the option to provide a value -- to the caller of 'signal' and continue execution there, or do what regular exception handlers -- do and continue execution after the handler. handleSignal :: forall a b c m. Monad m => (a -> m (ResumeOrBreak b c)) -> RuntimeImplemented (Signal a b) (ExceptT c m) c -> m c handleSignal f = fmap collapseEither . runExceptT . implement (SignalMethods h) where h a = do rb <- lift (f a) resumeOrBreak return throwE rb -- | This handler can only behave like a regular exception handler. If used along with 'throwSignal' -- this module behaves like regular checked exceptions. handleException :: forall a c m. Monad m => (a -> m c) -> ExceptT a m c -> m c handleException f = either f return <=< runExceptT -- | See documentation for 'handleException'. This handler gives you an 'Either'. handleToEither :: forall e a m. ExceptT e m a -> m (Either e a) handleToEither = runExceptT -- | Discard all the 'Throw' and 'Signal' effects. If any exception was thrown -- the result will be 'Nothing'. discardAllExceptions :: MaybeT m a -> m (Maybe a) discardAllExceptions = runMaybeT mapLeft :: (a -> c) -> Either a b -> Either c b mapLeft f (Left a) = Left (f a) mapLeft _ (Right b) = Right b -- | Satisfies all the 'Throw' and 'Signal' constraints /if/ they all throw 'Show'able -- exceptions. The first thrown exception will be shown and returned as a 'Left' result. showAllExceptions :: Functor m => ExceptT SomeSignal m a -> m (Either Text a) showAllExceptions = fmap (mapLeft getSomeSignal) . runExceptT newtype HandleException e m = HandleExceptionMethods { _handleWithoutDiscarding :: forall a. (e -> m a) -> m a -> m a } instance Effect (HandleException e) where type CanLift (HandleException e) t = RunnableTrans t liftThrough :: forall t m. (CanLift (HandleException e) t, Monad m, Monad (t m)) => HandleException e m -> HandleException e (t m) liftThrough (HandleExceptionMethods rec') = HandleExceptionMethods $ \f e -> do st <- currentTransState res <- lift (rec' (\ex -> runTransformer (f ex) st) (runTransformer e st)) restoreTransState res mergeContext m = HandleExceptionMethods $ \f ex -> do g <- _handleWithoutDiscarding <$> m g f ex -- | Use this function to handle exceptions without discarding the 'Throw' effect. -- You'll want to use this if you're writing a recursive function. Using the regular handlers -- in that case will result with infinite types. -- -- Since this function doesn't discard constraints, you still need to handle the exception on -- the whole computation. -- -- Here's a slightly contrived example. -- -- @ -- data NotFound = NotFound -- data Tree a = Leaf a | Node (Tree a) (Tree a) -- data Step = GoLeft | GoRight -- findIndex :: (Handles NotFound m, Eq a) => a -> Tree a -> m [Step] -- findIndex x (Leaf a) | x == a = return [] -- | otherwise = throwSignal NotFound -- findIndex x (Node l r) = ((GoLeft :) <$> findIndex x l) -- & handleWithoutDiscarding (\NotFound -> (GoRight :) <$> findIndex x r) -- @ -- -- Note: When you finally handle the exception effect, the order in which you handle it and -- other effects determines whether 'handleWithoutDiscarding' rolls back other effects if an exception -- occured or it preserves all of them up to the point of the exception. -- Handling exceptions last and handling them first will produce the former and latter -- behaviour respectively. handleWithoutDiscarding :: forall e m a. MonadEffect (HandleException e) m => (e -> m a) -> m a -> m a HandleExceptionMethods handleWithoutDiscarding = effect instance Monad m => MonadEffect (HandleException e) (ExceptT e m) where effect = HandleExceptionMethods $ \f -> ExceptT . (either (runExceptT . f) (return . Right) <=< runExceptT) -- | 'handleToEither' that doesn't discard 'Throws' constraints. See documentation for -- 'handleWithoutDiscarding'. handleToEitherRecursive :: MonadEffect (HandleException e) m => m a -> m (Either e a) handleToEitherRecursive = handleWithoutDiscarding (return . Left) . fmap Right