cleff-0.3.1.0: src/Cleff.hs
{-# 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'.