-- | This module defines the 'M' wrapper monad and the 'Eff' phantom constraint. All safe functionalities in this
-- module are reexported in the "Avail" module, so you wouldn't need to import this module most of the times.
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# OPTIONS_HADDOCK not-home #-}
module Avail.Internal where
import Control.Monad.Fix (MonadFix)
import Control.Monad.Zip (MonadZip)
import Data.Kind (Constraint, Type)
import Data.Proxy (Proxy (Proxy))
import Unsafe.Coerce (unsafeCoerce)
#ifdef AVAIL_semigroupoids
import Data.Functor.Apply (Apply)
import Data.Functor.Bind (Bind (join, (>>-)))
#endif
-- | The 'M' monad transformer acts as a /barrier of effects/. For example, for a monad type @App@ and any
-- effect typeclass @MonadOvO@ that @App@ has an instance of, the constraint @Eff MonadOvO@ is required to perform
-- the methods of @MonadOvO@ in the monad @'M' App@ as defined for the @App@ monad.
--
-- In particular, 'M' is expected to be used on a __concrete__ monad instead of a /polymorphic/ one. This is
-- particularly good in terms of program performance, and generally means instead of writing this:
--
-- @
-- f :: 'Control.Monad.State.MonadState' 'Int' m => m ()
-- @
--
-- You should write
--
-- @
-- f :: 'Eff' ('Control.Monad.State.MonadState' 'Int') => 'M' App ()
-- @
--
-- where @App@ is a monad stack of your choice that has support of @'Control.Monad.State.MonadState' 'Int'@. This also
-- means there is no 'Control.Monad.Trans.Class.MonadTrans' instance for 'M'.
--
-- Note: __you should not define instances of 'M' for effect typeclasses directly by hand__ as that is error-prone
-- and may create holes in effect management. For defining instances of effect typeclasses for 'M', check out
-- the "Avail.Derive" module and specifically the 'Avail.Derive.avail' and 'Avail.Derive.avail'' TH functions.
--
-- Also keep in mind that typeclasses inside @mtl@, @exceptions@, @unliftio@, @monad-control@ and @capability@ work
-- with 'M' out-of-the-box so no instance for them is needed to be defined on 'M' /by you/.
newtype M m a = UnsafeLift (m a) -- ^ Unsafely lift an @m@ action into @'M' m@. This completely sidesteps the
-- effect management mechanism; __You should not use this.__
deriving newtype (Functor, Applicative, Monad, MonadFix, MonadZip)
#ifdef AVAIL_semigroupoids
deriving newtype instance Apply m => Apply (M m)
instance Bind m => Bind (M m) where
UnsafeLift m >>- f = UnsafeLift $ m >>- (unM . f)
join (UnsafeLift m) = UnsafeLift $ join $ unM <$> m
#endif
-- | The kind of /effect typeclasses/, i.e. those that define a set of operations on a monad. Examples include
-- 'Control.Monad.IO.Class.MonadIO' and 'Control.Monad.Reader.MonadReader'.
--
-- This type is the same as the 'Capability.Constraints.Capability' type in @capability@.
type Effect = (Type -> Type) -> Constraint
-- | Any 'Effect' being used with @avail@ should have an instance of this class. Specifically, this class stores
-- the /superclasses/ of effect typeclasses. For example, 'Control.Monad.IO.Unlift.MonadUnliftIO' has a superclass
-- 'Control.Monad.IO.Class.MonadIO'.
--
-- You won't need to define instances of this by hand; instead, use the 'Avail.Derive.avail'' Template Haskell function.
class KnownList (Superclasses e) => IsEff (e :: Effect) where
-- | The superclasses of this typeclass.
type Superclasses e :: [Effect]
-- | The /primitive/ phantom effect constraint that does not take superclasses into account. You should not use this
-- directly; use 'Eff' or 'Effs' instead. Additionally, you definitely shouldn't define instances for this class.
class Eff' (e :: Effect) where
-- | The dummy method of the phantom typeclass, to be instantiated via the reflection trick in 'rip''.
instEffect :: Proxy e
instEffect = error "unimplemented"
-- | The constraint that indicates an effect is available for use, i.e. you can perform methods defined by instances
-- of the effect typeclass @e@ in a 'M' monad.
type Eff (e :: Effect) = (Eff' e, Effs (Superclasses e))
-- | Convenient alias for @('Eff' e1, 'Eff' e2, ..., 'Eff' en)@.
type family Effs (es :: [Effect]) :: Constraint where
Effs '[] = ()
Effs (e ': es) = (Eff e, Effs es)
-- | The newtype wrapper used to circumvent the impredicative problem of GHC and perform the reflection trick in
-- 'rip''. You have no reason to use this directly.
newtype InstEff e a = InstEff (Eff' e => a)
-- | Brutally rip off an 'Eff'' constraint, a la
-- [the reflection trick](https://hackage.haskell.org/package/base-4.16.0.0/docs/Unsafe-Coerce.html#v:unsafeCoerce).
-- __This is highly unsafe__ in terms of effect management.
rip' :: forall e a. (Eff' e => a) -> a
rip' x = unsafeCoerce (InstEff @e x) Proxy
-- | Brutally rip off an 'Eff' constraint. This means 'rip''ing off the 'Eff'' constraint of the current 'Effect'
-- and then 'rips' off constraints of all 'Superclasses' recursively. __This is highly unsafe__ in terms of effect
-- management.
rip :: forall e a. IsEff e => (Eff e => a) -> a
rip x = rips @(Superclasses e) $ rip' @e x
-- | The list of effect typeclasses @es@ is known at compile time. This is required for functions like 'runM'.
class KnownList (es :: [Effect]) where
-- | Brutally rip off many 'Eff' constraints. __This is highly unsafe__ in terms of effect management.
rips :: (Effs es => a) -> a
rips _ = error "unimplemented"
instance KnownList '[] where
rips x = x
instance (IsEff e, KnownList es) => KnownList (e ': es) where
rips x = rips @es $ rip @e x
-- | Unwrap the 'M' monad into the underlying concrete monad. This is rarely needed as most of the time you would also
-- want to eliminate 'Eff' constraints at the same time; for that see 'runM'.
unM :: M m a -> m a
unM (UnsafeLift m) = m
-- | Unwrap the 'M' monad into the underlying concrete monad and also eliminating 'Eff' constraints. You need
-- @TypeApplications@ in order to specify the list of 'Effect's you want to eliminate 'Eff' constraints for:
--
-- @
-- 'runM' @'[MonadReader Env, MonadState Store, MonadError MyErr] app
-- @
--
-- Note that functions like '(Data.Function.&)' generally does not work with this function; either apply directly or
-- use '($)' only.
runM :: forall es m a. KnownList es => (Effs es => M m a) -> m a
runM m = rips @es $ unM m