{-# LANGUAGE Trustworthy #-} -- | -- Copyright: (c) 2021 Xy Ren -- License: BSD3 -- Maintainer: xy.r@outlook.com -- Stability: experimental -- Portability: non-portable (GHC only) -- -- 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 reading 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 'Cleff.Reader.Reader' and 'Cleff.State.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. -- -- In terms of structuring your application, this library helps 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 enforce what effects are involved in each function, and avoid accidentally introduced behaviors. -- * __Effect decoupling:__ You can swap between the implementations of the effects in your application easily, -- so you can refactor and test your applications with less clutter. module Cleff ( -- * Using effects Eff , (:>) , (:>>) , Effect , IOE -- ** Running effects -- $runningEffects , runPure , runIOE -- * Defining effects -- $definingEffects , send , sendVia , makeEffect , makeEffect_ -- * Trivial effects handling , raise , raiseN , inject , subsume , subsumeN , KnownList , Subset -- * Interpreting effects -- $interpretingEffects , Handler , interpret , reinterpret , reinterpret2 , reinterpret3 , reinterpretN , interpose , impose , imposeN -- ** Interpreting in terms of 'IO' , HandlerIO , interpretIO -- ** Translating effects , Translator , transform , translate -- ** Transforming interpreters , raiseUnder , raiseNUnder , raiseUnderN , raiseNUnderN -- * Combinators for interpreting higher order effects -- $higherOrderEffects , Handling , toEff , toEffWith , withFromEff -- ** Interpreting 'IO'-related higher order effects , withToIO , fromIO -- * Miscellaneous , type (~>) , type (++) , MonadIO (..) , MonadUnliftIO (..) ) where import Cleff.Internal.Base import Cleff.Internal.Instances () import Cleff.Internal.Interpret import Cleff.Internal.Monad import Cleff.Internal.TH import UnliftIO (MonadIO (liftIO), MonadUnliftIO (withRunInIO)) -- $runningEffects -- To run an effect @T@, we should use an /interpreter/ of @T@, which is a function that has a 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 out of the box in their respective modules. -- -- By applying interpreters to an 'Eff' computation, you can eventually obtain an /end computation/, where there are no -- more effects to be interpreted 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 unwrapped into an IO computation via -- 'runIOE'. -- $definingEffects -- 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 and writing files can be like this: -- -- @ -- data Filesystem :: 'Effect' where -- ReadFile :: 'FilePath' -> Filesystem m 'String' -- WriteFile :: 'FilePath' -> 'String' -> Filesystem m () -- @ -- -- Here, @ReadFile@ is an operation that takes a 'FilePath' and returns a 'String', presumably the content of the file; -- @WriteFile@ is an operation that takes a 'FilePath' and a 'String' and returns @()@, meaning it only performs -- side effects - presumably writing the 'String' to the file specified. -- -- 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. -- $interpretingEffects -- An effect can be understood as the /syntax/ of a tiny language; however we also need to define the /meaning/ (or -- /semantics/) of the language. In other words, we need to specify the implementations 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. -- -- 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 -- @ -- -- Specifically, a @ReadFile@ operation is mapped to a real 'readFile' IO computation, and similarly a @WriteFile@ -- operation is mapped to a 'writeFile' computation. -- -- An effect is a set of abstract operations, and naturally, they can have more than one interpretations. Therefore, -- here we can also construct an in-memory filesystem that reads from and writes into a 'Cleff.State.State' effect, via -- the 'reinterpret' function that adds another effect to the stack for the effect handler to use: -- -- @ -- filesystemToState -- :: 'Cleff.Fail.Fail' ':>' es -- => 'Eff' (Filesystem : es) a -- -> 'Eff' ('Cleff.State.State' ('Data.Map.Map' 'FilePath' 'String') : es) a -- filesystemToState = 'reinterpret' \\case -- ReadFile path -> 'Cleff.State.gets' ('Data.Map.lookup' path) >>= \\case -- 'Nothing' -> 'fail' ("File not found: " ++ 'show' path) -- 'Just' contents -> 'pure' contents -- WriteFile path contents -> 'Cleff.State.modify' ('Data.Map.insert' path contents) -- @ -- -- Here, we used the 'reinterpret' function to introduce a @'Cleff.State.State' ('Data.Map.Map' 'FilePath' 'String')@ as -- the in-memory filesystem, making 'filesystemToState' a /reinterpreter/ that "maps" an effect into another effect. -- We also added a @'Cleff.Fail.Fail' ':>' es@ constraint to our reinterpreter so that we're able to report errors. -- To make an /interpreter/ out of this is simple, as we just need to interpret the remaining 'Cleff.State.State' -- effect: -- -- @ -- runFilesystemPure -- :: 'Cleff.Fail.Fail' ':>' es -- => 'Data.Map.Map' 'FilePath' 'String' -- -> 'Eff' (Filesystem : es) a -- -> 'Eff' es a -- runFilesystemPure fs -- = 'fmap' 'fst' -- runState returns (Eff es (a, s)), so we need to extract the first component to get (Eff es a) -- . 'Cleff.State.runState' fs -- (State (Map FilePath String) : es) ==> es -- . 'filesystemToState' -- (Filesystem : es) ==> (State (Map FilePath String) : es) -- @ -- -- Both of these interpreters can then be applied to computations with the @Filesystem@ effect to give different -- implementations to the effect. -- $higherOrderEffects -- /Higher order effects/ are effects whose operations take other effect computations as arguments. For example, the -- 'Cleff.Error.Error' effect is a higher order effect, because its 'Cleff.Error.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 -- 'Cleff.State.State', does not. -- -- It is harder to write interpreters for higher order effects, because the operations of these effects carry -- computations from arbitrary effect stacks, and we'll need to convert the to the current effect stack that the effect -- is being interpreted into. Fortunately, Cleff 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'.