cleff-0.2.0.0: Fast and concise extensible effects
Copyright (c) 2021 Xy Ren BSD3 xy.r@outlook.com experimental non-portable (GHC only) Trustworthy Haskell2010

Cleff

Description

This library implements an extensible effects system, where sets of monadic actions ("effects") are encoded as datatypes, tracked at the type level and can have multiple different implementations. This means you can swap out implementations of certain monadic actions in mock tests or in different environments. The notion of "effect" is general here: it can be an IO-performing side effect, or just obtaining the value of a static global environment.

In particular, this library consists of

• The Eff monad, which is the core of an extensible effects system. All effects are performed within it and it will be the "main" monad of your application. This monad tracks effects at the type level.
• A set of predefined general effects, like Reader and State that can be used out of the box.
• Combinators for defining new effects and interpreting them on your own. These effects can be translated in terms of other already existing effects, or into operations in the IO monad.

So, this library allows you to do two things:

• Effect management: The Eff monad tracks what effects are used explicitly at the type level, therefore you are able to be certain about what effects are involved in each function.
• Effect decoupling: You can decouple the implementation of the effects from your application and swap them easily.
Synopsis

# Using effects

data Eff es a Source #

The extensible effect monad. A monad Eff es is capable of performing any effect in the effect stack es, which is a type-level list that holds all effects available. However, most of the times, for flexibility, es should be a polymorphic type variable, and you should use the (:>) and (:>>) operators in constraints to indicate what effects are in the stack. For example,

Reader String :> es, State Bool :> es => Eff es Integer


allows you to perform operations of the Reader String effect and the State Bool effect in a computation returning an Integer.

#### Instances

Instances details
 IOE :> es => MonadBase IO (Eff es) Source # Compatibility instance; use MonadIO if possible. Instance detailsDefined in Cleff.Internal.Base MethodsliftBase :: IO α -> Eff es α # IOE :> es => MonadBaseControl IO (Eff es) Source # Compatibility instance; use MonadUnliftIO if possible. Instance detailsDefined in Cleff.Internal.Base Associated Typestype StM (Eff es) a # MethodsliftBaseWith :: (RunInBase (Eff es) IO -> IO a) -> Eff es a #restoreM :: StM (Eff es) a -> Eff es a # Monad (Eff es) Source # Instance detailsDefined in Cleff.Internal.Monad Methods(>>=) :: Eff es a -> (a -> Eff es b) -> Eff es b #(>>) :: Eff es a -> Eff es b -> Eff es b #return :: a -> Eff es a # Functor (Eff es) Source # Instance detailsDefined in Cleff.Internal.Monad Methodsfmap :: (a -> b) -> Eff es a -> Eff es b #(<$) :: a -> Eff es b -> Eff es a # MonadFix (Eff es) Source # Instance detailsDefined in Cleff.Internal.Monad Methodsmfix :: (a -> Eff es a) -> Eff es a # Fail :> es => MonadFail (Eff es) Source # Instance detailsDefined in Cleff.Fail Methodsfail :: String -> Eff es a # Applicative (Eff es) Source # Instance detailsDefined in Cleff.Internal.Monad Methodspure :: a -> Eff es a #(<*>) :: Eff es (a -> b) -> Eff es a -> Eff es b #liftA2 :: (a -> b -> c) -> Eff es a -> Eff es b -> Eff es c #(*>) :: Eff es a -> Eff es b -> Eff es b #(<*) :: Eff es a -> Eff es b -> Eff es a # IOE :> es => MonadIO (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base MethodsliftIO :: IO a -> Eff es a # IOE :> es => MonadThrow (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base MethodsthrowM :: Exception e => e -> Eff es a # IOE :> es => MonadCatch (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base Methodscatch :: Exception e => Eff es a -> (e -> Eff es a) -> Eff es a # IOE :> es => MonadMask (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base Methodsmask :: ((forall a. Eff es a -> Eff es a) -> Eff es b) -> Eff es b #uninterruptibleMask :: ((forall a. Eff es a -> Eff es a) -> Eff es b) -> Eff es b #generalBracket :: Eff es a -> (a -> ExitCase b -> Eff es c) -> (a -> Eff es b) -> Eff es (b, c) # IOE :> es => PrimMonad (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base Associated Typestype PrimState (Eff es) # Methodsprimitive :: (State# (PrimState (Eff es)) -> (# State# (PrimState (Eff es)), a #)) -> Eff es a # IOE :> es => MonadUnliftIO (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base MethodswithRunInIO :: ((forall a. Eff es a -> IO a) -> IO b) -> Eff es b # Semigroup a => Semigroup (Eff es a) Source # Instance detailsDefined in Cleff.Internal.Monad Methods(<>) :: Eff es a -> Eff es a -> Eff es a #sconcat :: NonEmpty (Eff es a) -> Eff es a #stimes :: Integral b => b -> Eff es a -> Eff es a # Monoid a => Monoid (Eff es a) Source # Instance detailsDefined in Cleff.Internal.Monad Methodsmempty :: Eff es a #mappend :: Eff es a -> Eff es a -> Eff es a #mconcat :: [Eff es a] -> Eff es a # type PrimState (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base type PrimState (Eff es) = RealWorld type StM (Eff es) a Source # Instance detailsDefined in Cleff.Internal.Base type StM (Eff es) a = a type (:>) = Elem infix 0 Source # e :> es means the effect e is present in the effect stack es, and therefore can be used in an Eff es computation. type family xs :>> es :: Constraint where ... infix 0 Source # xs :>> es means the list of effects xs are all present in the effect stack es. This is a convenient type alias for (e1 :> es, ..., en :> es). Equations  '[] :>> _ = () (x ': xs) :>> es = (x :> es, xs :>> es) type Effect = (Type -> Type) -> Type -> Type Source # The type of effects. An effect e m a takes an effect monad type m :: Type -> Type and a result type a :: Type. data IOE :: Effect Source # The effect for lifting and unlifting the IO monad, allowing you to use MonadIO, MonadUnliftIO, PrimMonad, MonadCatch, MonadThrow and MonadMask functionalities. This is the "final" effect that most effects eventually are interpreted into. For example, you can do: log :: IOE :> es => Eff es () log = liftIO (putStrLn "Test logging")  It is not recommended to use this effect in application code, as it is too liberal and allows arbitrary IO. Ideally, this is only used in interpreting more fine-grained effects. Note that this is not a real effect and cannot be interpreted in any way besides thisIsPureTrustMe and runIOE. It is similar to Polysemy's Final effect which also cannot be interpreted. This is mainly for performance concern, but also that there doesn't really exist reasonable interpretations other than the current one, given the underlying implementation of the Eff monad. IOE can be a real effect though, and you can enable the dynamic-ioe build flag to have that. However it is only for reference purposes and should not be used in production code. ## Running effects To run an effect T, we should use an interpreter of T, which is a function that has type like this: runT :: Eff (T ': es) a -> Eff es a  Such an interpreter provides an implementation of T and eliminates T from the effect stack. All builtin effects in cleff have interpreters coming together with them. By applying interpreters to an Eff computation, you can eventually obtain an end computation, where there are no more effects present on the effect stack. There are two kinds of end computations: • A pure computation with the type Eff '[] a, which you can obtain the value via runPure; or, • An impure computation with type Eff '[IOE] a that can be transformed into an IO computation via runIOE. runPure :: Eff '[] a -> a Source # Unwrap a pure Eff computation into a pure value, given that all effects are interpreted. Unwrap an Eff computation with side effects into an IO computation, given that all effects other than IOE are interpreted. # Defining effects An effect should be defined as a GADT and have the kind Effect. Each operation in the effect is a constructor of the effect type. For example, an effect supporting reading/writing files can be as following: data Filesystem :: Effect where ReadFile :: FilePath -> Filesystem m String WriteFile :: FilePath -> String -> Filesystem m ()  Operations constructed with these constructors can be performed via the send function. You can also use the Template Haskell function makeEffect to automatically generate definitions of functions that perform the effects. For example, makeEffect ''Filesystem  generates the following definitions: readFile :: Filesystem :> es => FilePath -> Eff es String readFile x = send (ReadFile x) writeFile :: Filesystem :> es => FilePath -> String -> Eff es () writeFile x y = send (WriteFile x y)  send :: e :> es => e (Eff es) ~> Eff es Source # Perform an effect operation, i.e. a value of an effect type e :: Effect. This requires e to be in the effect stack. sendVia :: e :> es' => (Eff es ~> Eff es') -> e (Eff es) ~> Eff es' Source # Perform an action in another effect stack via a transformation to that stack; in other words, this function "maps" the effect operation from effect stack es to es'. This is a generalization of send; end users most likely won't need to use this. send = sendVia id  For a datatype T representing an effect, makeEffect T generates functions defintions for performing the operations of T via send. The naming rule is changing the first uppercase letter in the constructor name to lowercase or removing the : symbol in the case of operator constructors. Also, this function will preserve any fixity declarations defined on the constructors. Because of the limitations of Template Haskell, all constructors of T should be polymorphic in the monad type, if they are to be used by makeEffect. For example, this is not OK: data Limited :: Effect where Noop :: Limited (Eff es) ()  because the monad type Eff es is not a fully polymorphic type variable. This function is also "weaker" than polysemy's makeSem, because this function cannot properly handle some cases involving complex higher order effects. Those cases are rare, though. See the tests for more details. Like makeEffect, but doesn't generate type signatures. This is useful when you want to attach Haddock documentation to the function signature, e.g.: data Identity :: Effect where Noop :: Identity m () makeEffect_ ''Identity -- | Perform nothing at all. noop :: Identity :> es => Eff es ()  Be careful that the function signatures must be added after the makeEffect_ call. # Trivial effects handling raise :: forall e es. Eff es ~> Eff (e ': es) Source # Lift a computation into a bigger effect stack with one more effect. For a more general version see raiseN. raiseN :: forall es' es. KnownList es' => Eff es ~> Eff (es' ++ es) Source # Lift a computation into a bigger effect stack with arbitrarily more effects. This function requires TypeApplications. inject :: forall es' es. Subset es' es => Eff es' ~> Eff es Source # Lift a computation with a fixed, known effect stack into some superset of the stack. subsume :: forall e es. e :> es => Eff (e ': es) ~> Eff es Source # Eliminate a duplicate effect from the top of the effect stack. For a more general version see subsumeN. subsumeN :: forall es' es. Subset es' es => Eff (es' ++ es) ~> Eff es Source # Eliminate several duplicate effects from the top of the effect stack. This function requires TypeApplications. class KnownList (es :: [k]) # The list es list is concrete, i.e. is of the form '[a1, a2, ..., an], i.e. is not a type variable. #### Instances Instances details  KnownList ('[] :: [k]) Instance detailsDefined in Data.Rec.SmallArray Methods KnownList es => KnownList (e ': es :: [k]) Instance detailsDefined in Data.Rec.SmallArray Methods class KnownList es => Subset (es :: [k]) (es' :: [k]) # es is a subset of es'. #### Instances Instances details  Subset ('[] :: [k]) (es :: [k]) Instance detailsDefined in Data.Rec.SmallArray MethodsreifyIndices :: [Int] (Subset es es', Elem e es') => Subset (e ': es :: [k]) (es' :: [k]) Instance detailsDefined in Data.Rec.SmallArray MethodsreifyIndices :: [Int] # Interpreting effects An effect can be understood as the "grammar" (or syntax) of a small language; however we also need to define the "meaning" (or semantics) of the language. In other words, we need to specify the implementation of effects. In an extensible effects system, this is achieved by writing effect handlers, which are functions that transforms operations of one effect into other "more primitive" effects. These handlers can then be used to make interpreters with library functions that we'll now see. This is very easy to do. For example, for the Filesystem effect data Filesystem :: Effect where ReadFile :: FilePath -> Filesystem m String WriteFile :: FilePath -> String -> Filesystem m ()  We can easily handle it in terms of IO operations via interpretIO, by pattern matching on the effect constructors: runFilesystemIO :: IOE :> es => Eff (Filesystem ': es) a -> Eff es a runFilesystemIO = interpretIO \case ReadFile path -> readFile path WriteFile path contents -> writeFile path contents  Alternatively, we can also construct an in-memory filesystem in terms of the State effect via the reinterpret function. runFilesystemPure :: Fail :> es => Map FilePath String -> Eff (Filesystem ': es) a -> Eff es a runFilesystemPure fs = fmap fst . runState fs . reinterpret \case ReadFile path -> gets (lookup path) >>= \case Nothing -> fail ("File not found: " ++ show path) Just contents -> pure contents WriteFile path contents -> modify (insert path contents)  These interpreters can then be applied to computations with the Filesystem effect to give different implementations to the effect. type Handler e es = forall esSend. Handling esSend e es => e (Eff esSend) ~> Eff es Source # The type of an effect handler, which is a function that transforms an effect e from an arbitrary effect stack into computations in the effect stack es. interpret :: forall e es. Handler e es -> Eff (e ': es) ~> Eff es Source # Interpret an effect e in terms of effects in the effect stack es with an effect handler. reinterpret :: forall e' e es. Handler e (e' ': es) -> Eff (e ': es) ~> Eff (e' ': es) Source # Like interpret, but adds a new effect e' that can be used in the handler. reinterpret2 :: forall e' e'' e es. Handler e (e' ': (e'' ': es)) -> Eff (e ': es) ~> Eff (e' ': (e'' ': es)) Source # Like reinterpret, but adds two new effects. reinterpret3 :: forall e' e'' e''' e es. Handler e (e' ': (e'' ': (e''' ': es))) -> Eff (e ': es) ~> Eff (e' ': (e'' ': (e''' ': es))) Source # Like reinterpret, but adds three new effects. reinterpretN :: forall es' e es. KnownList es' => Handler e (es' ++ es) -> Eff (e ': es) ~> Eff (es' ++ es) Source # Like reinterpret, but adds arbitrarily many new effects. This function requires TypeApplications. interpose :: forall e es. e :> es => Handler e es -> Eff es ~> Eff es Source # Respond to an effect while being able to leave it unhandled (i.e. you can resend the effects in the handler). impose :: forall e' e es. e :> es => Handler e (e' ': es) -> Eff es ~> Eff (e' ': es) Source # Like interpose, but allows to introduce one new effect to use in the handler. imposeN :: forall es' e es. (KnownList es', e :> es) => Handler e (es' ++ es) -> Eff es ~> Eff (es' ++ es) Source # Like impose, but allows introducing arbitrarily many effects. This requires TypeApplications. ## Interpreting in terms of IO type HandlerIO e es = forall esSend. Handling esSend e es => e (Eff esSend) ~> IO Source # The type of an IO effect handler, which is a function that transforms an effect e into IO computations. This is used for interpretIO. interpretIO :: IOE :> es => HandlerIO e es -> Eff (e ': es) ~> Eff es Source # Interpret an effect in terms of IO, by transforming an effect into IO computations. interpretIO f = interpret (liftIO . f)  ## Translating effects type Translator e e' = forall esSend. e (Eff esSend) ~> e' (Eff esSend) Source # The type of a simple transformation function from effect e to e'. transform :: forall e e' es. e' :> es => Translator e e' -> Eff (e ': es) ~> Eff es Source # Interpret an effect in terms of another effect in the stack via a simple Translator. transform trans = interpret (sendVia toEff . trans)  translate :: forall e e' es. Translator e e' -> Eff (e ': es) ~> Eff (e' ': es) Source # Like transform, but instead of using an effect in stack, add a new one to the top of it. translate trans = reinterpret (sendVia toEff . trans)  ## Transforming interpreters raiseUnder :: forall e' e es. Eff (e ': es) ~> Eff (e ': (e' ': es)) Source # Like raise, but adds the new effect under the top effect. This is useful for transforming an interpreter e' :> es => Eff (e ': es) ~> Eff es into a reinterpreter Eff (e ': es) ~> Eff (e' ': es): myInterpreter :: Bar :> es => Eff (Foo ': es) ~> Eff es myInterpreter = ... myReinterpreter :: Eff (Foo ': es) ~> Eff (Bar ': es) myReinterpreter = myInterpreter . raiseUnder  In other words, reinterpret h == interpret h . raiseUnder  However, note that this function is suited for transforming an existing interpreter into a reinterpreter; if you want to define a reinterpreter from scratch, you should still prefer reinterpret, which is both easier to use and more efficient. raiseNUnder :: forall es' e es. KnownList es' => Eff (e ': es) ~> Eff (e ': (es' ++ es)) Source # Like raiseUnder, but allows introducing multiple effects. This function requires TypeApplications. raiseUnderN :: forall e es' es. KnownList es' => Eff (es' ++ es) ~> Eff (es' ++ (e ': es)) Source # Like raiseUnder, but allows introducing the effect under multiple effects. This function requires TypeApplications. raiseNUnderN :: forall es'' es' es. (KnownList es', KnownList es'') => Eff (es' ++ es) ~> Eff (es' ++ (es'' ++ es)) Source # A generalization of both raiseUnderN and raiseNUnder, allowing introducing multiple effects under multiple effects. This function requires TypeApplications and is subject to serious type ambiguity; you most likely will need to supply all three type variables explicitly. # Combinators for interpreting higher order effects Higher order effects are effects whose operations take other effect computations as arguments. For example, the Error effect is a higher order effect, because its CatchError operation takes an effect computation that may throw errors and also an error handler that returns an effect computation: data Error e :: Effect where ThrowError :: e -> Error e m a CatchError :: m a -> (e -> m a) -> Error e m a  More literally, an high order effect makes use of the monad type paramenter m, while a first order effect, like State, does not. It is harder to write interpreters for higher order effects, because we need to transform computations from arbitrary effect stacks into a specific stack that the effect is currently interpreted into. In other words, they need to thread other effects through themselves. This is why Cleff also provides convenient combinators for doing so. In a Handler, you can temporarily "unlift" a computation from an arbitrary effect stack into the current stack via toEff, explicitly change the current effect interpretation in the computation via toEffWith, or directly express the effect in terms of IO via withToIO. class Handling esSend e es | esSend -> e es Source # The typeclass that denotes a handler scope, handling effect e sent from the effect stack esSend in the effect stack es. You should not define instances for this typeclass whatsoever. toEff :: Handling esSend e es => Eff esSend ~> Eff es Source # Run a computation in the current effect stack. This is useful for interpreting higher-order effects, like a bracketing effect: data Resource m a where Bracket :: m a -> (a -> m ()) -> (a -> m b) -> Resource m b  Bracket alloc dealloc use -> bracket (toEff alloc) (toEff . dealloc) (toEff . use)  toEffWith :: forall esSend e es. Handling esSend e es => Handler e es -> Eff esSend ~> Eff es Source # Run a computation in the current effect stack, but handles the current effect inside the computation differently by providing a new Handler. This is useful for interpreting effects with local contexts, like Local: runReader :: r -> Eff (Reader r ': es) ~> Eff es runReader x = interpret (handle x) where handle :: r -> Handler (Reader r) es handle r = \case Ask -> pure r Local f m -> toEffWith (handle$ f r) m


withFromEff :: Handling esSend e es => ((Eff es ~> Eff esSend) -> Eff esSend a) -> Eff es a Source #

Temporarily gain the ability to lift some Eff es actions into Eff esSend. This is useful for dealing with effect operations with the monad type in the negative position, which means it's unlikely that you need to use this function in implementing your effects.

## Interpreting IO-related higher order effects

withToIO :: (Handling esSend e es, IOE :> es) => ((Eff esSend ~> IO) -> IO a) -> Eff es a Source #

Temporarily gain the ability to unlift an Eff esSend computation into IO. This is useful for dealing with higher-order effects that involves IO.

fromIO :: (Handling esSend e es, IOE :> es) => IO ~> Eff esSend Source #

Lift an IO computation into Eff esSend. This is useful for dealing with effect operations with the monad type in the negative position within IOE, like masking.

# Miscellaneous

type (~>) (f :: k -> Type) (g :: k -> Type) = forall (a :: k). f a -> g a infixr 0 #

The type of natural transformations from functor f to g.

type family (xs :: [a]) ++ (ys :: [a]) :: [a] where ... infixr 5 #

Type level list concatenation.

Equations

 ('[] :: [a]) ++ (ys :: [a]) = ys (x ': xs :: [a]) ++ (ys :: [a]) = x ': (xs ++ ys)

class Monad m => MonadIO (m :: Type -> Type) where #

Monads in which IO computations may be embedded. Any monad built by applying a sequence of monad transformers to the IO monad will be an instance of this class.

Instances should satisfy the following laws, which state that liftIO is a transformer of monads:

• liftIO . return = return
• liftIO (m >>= f) = liftIO m >>= (liftIO . f)

Methods

liftIO :: IO a -> m a #

Lift a computation from the IO monad.

#### Instances

Instances details

class MonadIO m => MonadUnliftIO (m :: Type -> Type) where #

Monads which allow their actions to be run in IO.

While MonadIO allows an IO action to be lifted into another monad, this class captures the opposite concept: allowing you to capture the monadic context. Note that, in order to meet the laws given below, the intuition is that a monad must have no monadic state, but may have monadic context. This essentially limits MonadUnliftIO to ReaderT and IdentityT transformers on top of IO.

Laws. For any value u returned by askUnliftIO, it must meet the monad transformer laws as reformulated for MonadUnliftIO:

• unliftIO u . return = return
• unliftIO u (m >>= f) = unliftIO u m >>= unliftIO u . f

Instances of MonadUnliftIO must also satisfy the idempotency law:

• askUnliftIO >>= \u -> (liftIO . unliftIO u) m = m

This law showcases two properties. First, askUnliftIO doesn't change the monadic context, and second, liftIO . unliftIO u is equivalent to id IF called in the same monadic context as askUnliftIO.

Since: unliftio-core-0.1.0.0

Methods

withRunInIO :: ((forall a. m a -> IO a) -> IO b) -> m b #

Convenience function for capturing the monadic context and running an IO action with a runner function. The runner function is used to run a monadic action m in IO.

Since: unliftio-core-0.1.0.0

#### Instances

Instances details
 Instance detailsDefined in Control.Monad.IO.Unlift MethodswithRunInIO :: ((forall a. IO a -> IO a) -> IO b) -> IO b # IOE :> es => MonadUnliftIO (Eff es) Source # Instance detailsDefined in Cleff.Internal.Base MethodswithRunInIO :: ((forall a. Eff es a -> IO a) -> IO b) -> Eff es b # MonadUnliftIO m => MonadUnliftIO (ReaderT r m) Instance detailsDefined in Control.Monad.IO.Unlift MethodswithRunInIO :: ((forall a. ReaderT r m a -> IO a) -> IO b) -> ReaderT r m b # Instance detailsDefined in Control.Monad.IO.Unlift MethodswithRunInIO :: ((forall a. IdentityT m a -> IO a) -> IO b) -> IdentityT m b #