sr-extra-1.88: Extra/Errors.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Extra.Errors
( IsMember
, Nub
, OneOf(Empty, Val, NoVal)
, Set(set)
, Get(get)
, oneOf
, DeleteList
, DeleteOneOf
, Delete(delete)
, Member
, throwMember
, liftMember
, catchMember
, liftExceptT
, tryMember
, mapMember
, catchMember'
#if 0
, IsSubset
, throwMembers
, liftMembers
, liftExceptTs
#endif
, runNullExceptT
, runNullExcept
, liftUIO
, runOneOf -- I think this is the best one
, runOneOf'
, runOneOf''
, Errors
, Errors'
, test
, IOException
, module Control.Monad.Except
, module Extra.Except
, module UnexceptionalIO.Trans
) where
import Control.Exception (fromException, IOException, toException)
import Control.Lens (Prism', preview, prism', review)
import Control.Monad.Except (Except, ExceptT, lift, MonadError, runExcept, runExceptT, throwError, withExceptT)
--import Extra.Except (mapError)
import Data.Type.Bool
--import Data.Type.Equality
import Data.Word (Word8)
import Data.SafeCopy
import qualified Data.Serialize as S (Serialize(get, put), getWord8, Put, PutM, Get)
import Data.Typeable (Typeable, typeOf)
import Data.Proxy
import Extra.Except (NonIOException(..), tryError)
import GHC.Stack (HasCallStack)
import UnexceptionalIO.Trans (fromIO, SomeNonPseudoException, Unexceptional)
type family IsMember x ys where
IsMember x '[] = 'False
IsMember x (x ': ys) = 'True
IsMember x (y ': ys) = IsMember x ys
IsMember x ys = IsMember x ys
--type family Member x es where
-- Member' x (OneOf xs) = Member' x xs
type Member e es = (IsMember e es ~ 'True, Get e es, Set e es, Delete e es)
type family Nub xs where
Nub '[] = '[]
Nub (x ': ys) = If (IsMember x ys) ys (x ': Nub ys)
data OneOf (n :: [k]) where
Empty :: OneOf s
Val :: e -> OneOf (e ': s)
NoVal :: OneOf s -> OneOf (e ': s)
deriving instance Typeable k => Typeable (OneOf (n :: [k]))
instance Show (OneOf '[]) where
show Empty = "{}"
instance (Show e, Show (OneOf s)) => Show (OneOf (e ': s)) where
show (Val e) = show e
show (NoVal o) = show o
show Empty = "{}"
instance S.Serialize (OneOf '[]) where
get :: S.Get (OneOf s)
get = return Empty
put :: OneOf s -> S.PutM ()
put Empty = return ()
put _ = error "fix warning"
instance (S.Serialize e, S.Serialize (OneOf s)) => S.Serialize (OneOf (e ': s)) where
put :: OneOf (e ': s) -> S.PutM ()
put (NoVal o) = S.put (0 :: Word8) >> S.put o
put (Val e) = S.put (1 :: Word8) >> S.put e
put _ = error "impossible"
get :: S.Get (OneOf (e ': s))
get = S.getWord8 >>= \case
0 -> NoVal <$> S.get
1 -> Val <$> S.get
_ -> error "impossible"
instance Typeable k => SafeCopy (OneOf ('[] :: [k])) where
version = 1
kind = base
getCopy :: S.Serialize (OneOf s) => Contained (S.Get (OneOf s))
getCopy = contain S.get
putCopy :: S.Serialize (OneOf s) => OneOf s -> Contained S.Put
putCopy = contain . S.put
errorTypeName _ = "()"
instance (SafeCopy e, S.Serialize e, Typeable e, S.Serialize (OneOf s), Typeable s) => SafeCopy (OneOf (e ': s)) where
version = 1
kind = base
getCopy :: Contained (S.Get (OneOf (e ': s)))
getCopy = contain S.get
putCopy :: OneOf (e ': s) -> Contained S.Put
putCopy = contain . S.put
errorTypeName = show . typeOf
class Set e xs where
set :: e -> OneOf xs
instance Set e (e ': xs) where
set e = Val e
instance {-# OVERLAPS #-} (IsMember e xs ~ 'True, Set e xs) => Set e (f ': xs) where
set e = NoVal (set e)
class Get e xs where
get :: OneOf xs -> Maybe e
instance {-# OVERLAPS #-} Get e (e ': xs) where
get (Val e) = Just e
get (NoVal _) = Nothing
get Empty = error "impossible"
instance (IsMember e xs ~ 'True, Get e xs) => Get e (f ': xs) where
get (NoVal o) = get o
get (Val _e) = Nothing
get Empty = error "impossible"
oneOf :: (Get e es, Set e es) => Prism' (OneOf es) e
oneOf = prism' set get
type family DeleteList e xs where
DeleteList x '[] = '[]
DeleteList x (x ': ys) = ys
DeleteList x (y ': ys) = (y ': (DeleteList x ys))
type family DeleteOneOf e xs where
DeleteOneOf x (OneOf ys) = OneOf (DeleteList x ys)
class Delete e xs where
delete :: Proxy e -> OneOf xs -> DeleteOneOf e (OneOf xs)
instance Delete e (e ': xs) where
delete _ (Val _e) = Empty
delete _ (NoVal o) = o
delete _ Empty = Empty
instance {-# OVERLAPS #-} forall e f xs. (Delete e xs, DeleteList e (f:xs) ~ (f : DeleteList e xs)) => Delete e (f ': xs) where
delete _p (Val v) = (Val v) -- :: OneOf (f ': (DeleteList e xs))
delete p (NoVal o) = NoVal (delete p o)
delete _p Empty = Empty
throwMember :: forall e es m a. (Member e es, MonadError (OneOf es) m) => e -> m a
throwMember = throwError . review oneOf
liftMember :: forall e es m a. (Member e es, MonadError (OneOf es) m) => Either e a -> m a
liftMember = either throwMember return
liftExceptT :: (Member e es, MonadError (OneOf es) m) => ExceptT e m a -> m a
liftExceptT action = liftMember =<< runExceptT action
-- | Run an action with @e@ added to the current error set @es@.
-- Typically this is used by forcing the action into ExceptT with
-- the augmented error type:
-- @@
-- catchMember withExceptT (fileIO @(FileError ': e) (Right <$> query st (LookValue key))) (return . Left)
-- @@
catchMember ::
forall esplus e es m n a.
(Member e esplus, es ~ DeleteList e esplus,
MonadError (OneOf esplus) m, MonadError (OneOf es) n)
=> (forall b. (OneOf esplus -> OneOf es) -> m b -> n b)
-> m a -> (e -> n a) -> n a
catchMember helper ma f =
-- The idea here is that we use tryError to bring a copy of e into
-- the return value, then we can just delete e from error monad.
helper (delete @e Proxy) (tryError ma) >>= either handle return
where handle :: OneOf esplus -> n a
handle es = maybe (throwError (delete @e Proxy es)) f (get es :: Maybe e)
catchMember'' ::
forall e (es :: [*]) m a.
(Monad m)
=> ExceptT (OneOf (e ': es)) m a
-> ExceptT (OneOf es) m (Either e a)
catchMember'' m = do
lift (runExceptT m) >>= either (\es -> maybe (throwError (delete (Proxy @e) es :: OneOf es)) (pure . Left) (preview oneOf es)) (pure . Right)
catchMember' ::
forall e (es :: [*]) m a.
(MonadError (OneOf es) m)
=> ExceptT (OneOf (e ': es)) m a
-> (e -> m a)
-> m a
catchMember' action handle =
either throwError (either handle pure) =<< runExceptT (catchMember'' @e action)
#if 0
type family IsSubset xs ys where
IsSubset '[] _ = 'True
IsSubset xs '[] = 'False
IsSubset (x ': xs) ys = (IsMember x ys) && (IsSubset xs ys)
liftExceptTs :: (IsSubset es' es ~ 'True, MonadError (OneOf es) m) => ExceptT (OneOf es') m a -> m a
liftExceptTs action = liftMembers =<< runExceptT action
liftMembers :: forall es' es m a. (IsSubset es' es ~ 'True, MonadError (OneOf es) m) => Either (OneOf es') a -> m a
liftMembers = either throwMembers return
-- Special case of what we would really like.
throwMembers :: forall e es' es m a. (IsSubset es' es ~ 'True, MonadError (OneOf es) m) => OneOf es' -> m a
throwMembers Empty = error "throwMembers"
throwMembers (Val e) = throwError (review oneOf e :: OneOf es)
throwMembers (NoVal o) = throwMembers o
#endif
-- | Simplified catchMember where the monad doesn't change.
tryMember :: forall e es m a. (Member e es, MonadError (OneOf es) m) => m a -> (e -> m a) -> m a
tryMember ma f = tryError ma >>= either (\es -> maybe ma f (get es :: Maybe e)) return
-- | Annotate a member error that has been thrown.
mapMember :: forall e es m a. (Member e es, MonadError (OneOf es) m) => (e -> m e) -> m a -> m a
mapMember f ma =
tryError ma >>= either (\es -> maybe (throwError es) (\e -> f e >>= throwMember) (get es :: Maybe e)) return
runNullExceptT :: Functor m => ExceptT (OneOf '[]) m a -> m a
runNullExceptT m = (\(Right a) -> a) <$> runExceptT m
runNullExcept :: Except (OneOf '[]) a -> a
runNullExcept m = (\(Right a) -> a) (runExcept m)
liftUIO ::
(Unexceptional m, Member NonIOException e, Member IOException e, MonadError (OneOf e) m)
=> IO a
-> m a
liftUIO io =
runExceptT (fromIO io) >>= either (either throwMember throwMember . splitException) return
where
splitException :: SomeNonPseudoException -> Either NonIOException IOException
splitException e = maybe (Left (NonIOException e)) Right (fromException (toException e) :: Maybe IOException)
-- | Catch any FileError thrown and put it in the return value.
runOneOf'' ::
forall (esplus :: [*]) e (es :: [*]) m a.
(es ~ DeleteList e esplus, Member e esplus, Monad m)
=> ExceptT (OneOf esplus) m a
-> ExceptT (OneOf es) m (Either e a)
runOneOf'' action = catchMember withExceptT (Right <$> action) (return . Left)
runOneOf' ::
forall (esplus :: [*]) e (es :: [*]) m a r.
(es ~ DeleteList e esplus, Member e esplus, MonadError (OneOf es) m)
=> ExceptT (OneOf esplus) m a
-> (Either e a -> m r)
-> m r
runOneOf' action final = runExceptT (runOneOf'' action) >>= either throwError final
runOneOf ::
forall (esplus :: [*]) e (es :: [*]) m a.
(es ~ DeleteList e esplus, Member e esplus, MonadError (OneOf es) m)
=> ExceptT (OneOf esplus) m a
-> m (Either e a)
runOneOf action = runOneOf' action return
type Errors e = (Show (OneOf e), Typeable e, HasCallStack)
type Errors' e = (Show (OneOf e), Typeable e)
-- ** Example
data ErrorFoo = Foo deriving Show
data ErrorBar = Bar deriving Show
data ErrorBaz = Baz deriving Show
type AppErrors = OneOf '[ErrorFoo, ErrorBar, ErrorBaz]
handleBar :: (Member ErrorBar errors) => OneOf errors -> IO (DeleteOneOf ErrorBar (OneOf errors))
handleBar err =
do case get err of
Nothing -> putStrLn "no Bar error to handle"
(Just Bar) -> putStrLn "took care of Bar"
pure (delete @ErrorBar Proxy err)
handleFoo :: ({-IsMember ErrorFoo errors ~ 'True,-} Get ErrorFoo errors, Delete ErrorFoo errors) => OneOf errors -> IO (DeleteOneOf ErrorFoo (OneOf errors))
handleFoo err =
do case get err of
Nothing -> putStrLn "no Bar error to handle"
(Just Foo) -> putStrLn "took care of Bar"
pure (delete @ErrorFoo Proxy err)
{-
Generates the ouput:
current error = Foo
no Bar error to handle
current error = Foo
took care of Bar
current error = {}
-}
test :: IO ()
test =
do let errAll = set Foo :: AppErrors
putStrLn $ "current error = " ++ show errAll
errNoBar <- handleBar errAll
putStrLn $ "current error = " ++ show errNoBar
errNoFoo <- handleFoo errNoBar
putStrLn $ "current error = " ++ show errNoFoo
-- this will not compile because ErrorFoo has already been handled
-- errNoFoFoo <- handleFoo errNoFoo
pure ()