sr-extra-1.80: Extra/Errors.hs
{-# LANGUAGE ConstraintKinds #-}
{-# 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
, tryMember
, mapMember
, runNullExceptT
, runNullExcept
, test
) where
import Control.Lens (Prism', prism', review)
import Control.Monad.Except (Except, ExceptT, MonadError, runExcept, runExceptT, throwError)
--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 (tryError)
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)
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
_ -> error "impossible"
instance SafeCopy (OneOf '[]) 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 :: ({-IsMember e xs ~ 'True,-} Get e xs, Set e xs) => Prism' (OneOf xs) 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 :: (Member e es, MonadError (OneOf es) m) => e -> m a
throwMember = throwError . review oneOf
liftMember :: (Member e es, MonadError (OneOf es) m) => Either e a -> m a
liftMember = either throwMember return
-- | 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 e es es' m n a.
(Member e es, es' ~ DeleteList e es,
MonadError (OneOf es) m, MonadError (OneOf es') n)
=> (forall b. (OneOf es -> 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 es -> n a
handle es = maybe (throwError (delete @e Proxy es)) f (get es :: Maybe e)
-- | 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)
-- ** 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 ()