monad-effect: A fast and lightweight effect system.
A fast and lightweight effect system. It provides a way to define and handle effects and exceptions in a modular and composable way. Main features: moduled effects, algebraic exceptions, pure states, and good performance.
[Skip to Readme]
Modules
[Index] [Quick Jump]
Flags
Manual Flags
| Name | Description | Default |
|---|---|---|
| bencho0 | Compile benchmarks with -O0 | Disabled |
| bencho1 | Compile benchmarks with -O1 | Disabled |
| bencho2 | Compile benchmarks with -O2 | Disabled |
| noinline | Disable inlining for Countdown benchmark | Disabled |
Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info
Downloads
- monad-effect-0.2.3.0.tar.gz [browse] (Cabal source package)
- Package description (as included in the package)
Maintainer's Corner
For package maintainers and hackage trustees
Candidates
- No Candidates
| Versions [RSS] | 0.1.0.0, 0.2.0.0, 0.2.1.0, 0.2.2.0, 0.2.3.0 |
|---|---|
| Change log | CHANGELOG.md |
| Dependencies | async (<2.4), base (>=4 && <5), data-default (>=0.8.0 && <0.9), deepseq (<1.6), exceptions (<0.11), haskell-src-meta (>=0.8 && <0.9), monad-control (>=1.0.3 && <1.1), mtl (<2.4), parsec (>=3 && <4), resourcet (>=1.3.0 && <1.4), stm (<2.6), template-haskell (<2.24), text (<2.2), transformers-base (>=0.4.6 && <0.5) [details] |
| License | BSD-3-Clause |
| Author | Eiko |
| Maintainer | eikochanowo@outlook.com |
| Category | Control, Monads, Effect |
| Source repo | head: git clone https://github.com/Eiko-Tokura/monad-effect.git |
| Uploaded | by eiko at 2025-12-13T12:13:58Z |
| Distributions | NixOS:0.2.0.0 |
| Reverse Dependencies | 1 direct, 0 indirect [details] |
| Downloads | 35 total (14 in the last 30 days) |
| Rating | (no votes yet) [estimated by Bayesian average] |
| Your Rating |
|
| Status | Docs uploaded by user Build status unknown [no reports yet] |
Readme for monad-effect-0.2.3.0
[back to package description]monad-effect - a lightweight, fast, algebraic effect system
This project is still in experimental beta and may evolve quickly. Feedback and contributions are very welcome.
monad-effect gives you:
- a single, optimisation-friendly monad transformer
EffTthat combines Reader, State and algebraic errors; - modules as the unit of effect (e.g. reader, state, logging, database, HTTP, metrics);
- explicit, composable error lists instead of using
Text/SomeException; and - performant effect stacks, without sacrificing purity.
Most users will work with the Eff / EffT type aliases and the built-in reader/state modules (RModule, SModule) and define their own modules around them.
- Project Intuition
- Key Features
- Core Types and Abstractions
- Getting Started - Examples
- Selected API Reference
Project Intuition
At a high level you can think of:
newtype EffT mods es m a =
EffT { unEffT :: SystemRead mods -> SystemState mods -> m (Result es a, SystemState mods) }
as:
- one layer of
Readerover a heterogeneous environmentSystemRead mods, - one layer of
Stateover a heterogeneous stateSystemState mods, and - a typed, algebraic error channel
Result es a, wherees :: [Type]is a type-level list of error types.
You explicitly say:
- which modules (
mods) your effect depends on (configuration, mutable state, handles, etc.); - which errors (
es) it can throw (e.g.IOException,ErrorText "http",MyDomainError); and - you get back both a result and the final module state.
Typical use-cases:
- replace one or more layers of
ReaderT,StateT,ExceptTor even more equivalent ones, by a singleEffTwith a small list of modules and error types; - easily add or remove error types (
effCatch,effCatchIn,errorToEitherAll, ... ); - run the same effect in pure monads (e.g.
Identity) and inIO; and - pass around large module stacks in real applications while keeping the types informative and composable.
Key Features
Algebraic exceptions
In classic Haskell (and in other languages like Rust), exceptions are often encoded algebraically as Maybe or Either:
Maybe ais composable but not very informative - you lose any structured information about why something failed.Either e acarries an error payload, but composing multiple distinctEither e_i avalues across a codebase tends to either:- collapse everything to a common super-type like
Text/SomeException(and then you lose the ability to catch specific errors in a principled way, and loses the ability to declare that some of them won't happen); or - nest
Either e0 (Either e1 (Either e2 a)), which is unergonomic.
- collapse everything to a common super-type like
ExceptT e m ahas the same compositional issues, and the transformer order matters:StateT s (ExceptT e m) a ~ s -> m (Either e (a, s))- once an exception is thrown, bothaand the intermediate state are lost / rolled-back.ExceptT e (StateT s m) a ~ s -> m (Either e a, s)- the state up to the exception point is preserved, which is often what you actually want.
monad-effect addresses these issues by:
- using a type-level list of error types
es :: [Type]and a non-empty sumEList esto track exactly which error types can occur; and - using
Result es aas the algebraic error carrier (see the formal definition below), which behaves likeEither (EList es) aand collapses toawhenes ~ '[].
The underlying representation
SystemRead mods
-> SystemState mods
-> m (Result es a, SystemState mods)
means that module state is preserved when an algebraic exception is thrown (like ExceptT e (StateT s m) a), rather than discarded.
You can throw algebraic errors into the list (effThrowIn / effThrow), catch all errors (effCatchAll), or catch and remove a specific error type (effCatchIn) so that the remaining computation provably no longer produces that error.
Purity
Instead of reaching for IORef / TVar for every bit of mutable state, you can also choose to keep states pure and model them as part of your self-defined modules. It's a design choice you can make : some effect systems force you into IO. While for concurrency programs you need TVars, but we should have the ability to choose pure state where appropriate because we love purity.
In particular the library provides two built-in modules:
SModule s- a module holding a pure state of types;RModule r- a module holding a read-only value of typer.
These integrate with the MonadStateful / MonadReadable classes and provide the familiar getS / putS / modifyS / askR / localR APIs, while still participating in the larger module stack.
Using modules, you can:
- keep configuration, pure in-memory state, handles, and effect interpreters in a single typed module stack; and
- run the same code in
Identityfor pure tests, or inIOfor production, by choosing appropriate runners. - Use it to run stateful tight computations without
IOoverhead (which GHC can optimize very well).
Template Haskell helpers in Module.RS.QQ (makeRModule, makeRSModule) make it easy to generate simple reader/state modules with minimal boilerplate. However, right now you are expected to write a lot of modules by hand as they are much more flexible.
Besides tight calculation that benefits from pure states, here is another example function that benefits from pure state: the function can be ran as a pure function with pure logging effect (writer / no-logging), or in IO whose logging prints to console/file.
eventHandler
:: (Monad pureMonad)
=> InputEvent
-> EffT
'[ Logging pureMonad LogData -- ^ We will use logging to generate diagnostics
, EventState -- ^ We need to read and update the state
]
'[ ErrorText "not-allowed"
]
pureMonad
[OutputCommands] -- ^ Output commands from the event module
Flexible and modular
Because both modules and errors are tracked at the type level:
EffT '[] '[] m ais isomorphic tom a;EffT '[] '[e] m ais isomorphic tom (Either e a);EffT '[] es m ais isomorphic tom (Result es a).
You can:
-
eliminate modules once you have their inputs, using e.g.:
runEffTOuter :: (ConsFDataList c (mod : mods), ConsFData1 c mods, Monad m) => ModuleRead mod -> ModuleState mod -> EffT' c (mod : mods) es m a -> EffT' c mods es m (a, ModuleState mod) runEffTOuter_ :: ... => ModuleRead mod -> ModuleState mod -> EffT' c (mod : mods) es m a -> EffT' c mods es m aand similarly
runEffTIn/runEffTIn_to drop an inner module. -
embed smaller effects inside larger ones, changing only modules (
embedMods), only errors (embedError), or both (embedEffT). -
move fluidly between
EffTand more conventional forms via runners and converters likerunEffT00,runEffT01,errorToEither,errorToEitherAll,errorToMaybe, and theeffEither*/effMaybe*family of helpers.
The result is a small set of primitives that scale well to large applications with many modules such as database access, HTTP clients, metrics, logging, and domain-specific state.
Performant
The core transformer EffT' is one single layer and the FData data family is designed to be optimisation-friendly. The fact that we did not use complicated data structure nor IO for storing data means GHC can optimize, inline the data constructors, utilizing purity.
This gives blazingly fast performance, benchmarks (countdown, local state, deep catch) shows monad-effect is top-1 in most of the benchmarks, top-2 in some other ones. (See the last section for some benchmark pictures).
It is completely feasible to use monad-effect for pure single-threaded stateful tight loops with little or no extra overhead.
Core Types and Abstractions
This section spells out the main types and how they fit together. All snippets in this section are taken directly from the library, except where explicitly marked as simplified.
EffT, Eff and EffT'
The real core transformer is EffT'; Eff and EffT are its specialised aliases using the optimised FData container.
-- | EffTectful computation, using modules as units of effect.
-- The tick indicates the polymorphic type 'c', the data structure
-- used to store the modules (usually 'FData' or 'FList').
newtype EffT' (c :: (Type -> Type) -> [Type] -> Type)
(mods :: [Type])
(es :: [Type])
(m :: Type -> Type) a
= EffT'
{ unEffT' :: SystemRead c mods
-> SystemState c mods
-> m (Result es a, SystemState c mods)
}
-- Recommended, specialised aliases (use these in normal code):
type Eff mods es = EffT' FData mods es IO
type EffT mods es = EffT' FData mods es
type Pure mods es = EffT' FData mods es Identity
type In mods es = In' FData mods es
-- Error-enhanced IO and ExceptT-like transformer
type ResultT es m = EffT' FData '[] es m
type IO' es = EffT' FData '[] es IO
Intuitively:
mods :: [Type]- type-level list of modules (effects);es :: [Type]- type-level list of error types that this computation may throw;m- the base monad; andc- the container type (FDataby default) used to hold the module environments and states.
The library provides runners such as:
runEffT
:: Monad m
=> SystemRead c mods
-> SystemState c mods
-> EffT' c mods es m a
-> m (Result es a, SystemState c mods)
-- No modules
runEffT0
:: (Monad m, ConsFNil c)
=> EffT' c '[] es m a
-> m (Result es a)
-- No modules, no errors
type NoError = '[]
runEffT00
:: (Monad m, ConsFNil c)
=> EffT' c '[] NoError m a
-> m a
-- No modules, single error, exposed as Either
runEffT01
:: (Monad m, ConsFNil c)
=> EffT' c '[] '[e] m a
-> m (Either e a)
There are much more runners and combinators, see actual haddock documentation.
EffT and Eff can therefore be specialised to behave like a more flexible ExceptT:
-- A flexible replacement for 'ExceptT es m'
type ResultT es m = EffT '[] es m
Result and EList - algebraic error lists
Errors are represented by the Result and EList types:
-- | Sum of types, non-empty by construction.
data EList (ts :: [Type]) where
EHead :: !t -> EList (t : ts)
ETail :: !(EList ts) -> EList (t : ts)
-- | Error-aware result.
data Result (es :: [Type]) (a :: Type) where
RSuccess :: a -> Result es a
RFailure :: !(EList es) -> Result es a
resultNoError :: Result '[] a -> a
resultNoError (RSuccess a) = a
Important facts:
Result es abehaves likeEither (EList es) a.- When
es ~ '[],Result '[] ais effectively justa(resultNoErrorwitnesses this). EList esis a non-empty sum type: you cannot construct anEList '[], so aRFailurealways carries one of the listed error types.
Named error types - ErrorText, ErrorValue, MonadExcept
To avoid defining a new ADT for every small error case, the library provides ad-hoc, named error wrappers:
-- | A named textual error.
newtype ErrorText (s :: k) = ErrorText Text
deriving newtype (IsString)
-- | Use type application 'errorText @"http" "text"'
errorText :: forall s. Text -> ErrorText s
errorText = ErrorText
-- | A named error that wraps an arbitrary value.
newtype ErrorValue (a :: k) (v :: Type) = ErrorValue v
-- | Type application helper
errorValue :: forall s v. v -> ErrorValue s v
errorValue = ErrorValue
-- | MonadExcept without a functional dependency,
-- so a monad can throw multiple error types.
class Monad m => MonadExcept e m where
throwExcept :: e -> m a
Some useful instances:
instance Exception e => MonadExcept e IOinstance MonadExcept e (Either e)instance Monad m => MonadExcept e (ExceptT e m)instance KnownSymbol s => MonadExcept (ErrorText s) (Either (Text, Text))
This makes it very convenient to use ErrorText "http", ErrorText "decode" etc. in larger codebases.
Modules and the system view
Modules are the unit of effect. A module describes:
- what read-only data it exposes (
ModuleRead); and - what mutable state it keeps (
ModuleState).
class Module mod where
data ModuleRead mod :: Type
data ModuleState mod :: Type
-- Modules that may be part of a "system"
class Module mod => SystemModule mod where
data ModuleEvent mod :: Type
data ModuleInitData mod :: Type
-- System-wide containers (usually backed by 'FData')
type SystemRead c mods = c ModuleRead mods
type SystemState c mods = c ModuleState mods
type SystemEvent mods = UList ModuleEvent mods
type SystemInitData c mods = c ModuleInitData mods
Within EffT' you can access the data families, module reads and states using the provided helpers:
-- | Synonyms
queryModule :: (Monad m, In' c mod mods, Module mod)
=> EffT' c mods es m (ModuleRead mod)
askModule :: (Monad m, In' c mod mods, Module mod)
=> EffT' c mods es m (ModuleRead mod)
queriesModule, asksModule
:: (Monad m, In' c mod mods, Module mod)
=> (ModuleRead mod -> a)
-> EffT' c mods es m a
getModule :: (Monad m, In' c mod mods, Module mod)
=> EffT' c mods es m (ModuleState mod)
getsModule :: (Monad m, In' c mod mods, Module mod)
=> (ModuleState mod -> a) -> EffT' c mods es m a
putModule :: (Monad m, In' c mod mods, Module mod)
=> ModuleState mod -> EffT' c mods es m ()
modifyModule :: (Monad m, In' c mod mods, Module mod)
=> (ModuleState mod -> ModuleState mod)
-> EffT' c mods es m ()
The SystemModule/ModuleEvent/ModuleInitData pieces are primarily used by higher-level orchestration helpers (e.g. scoped initialisation via withModule).
Built-in Reader and State modules - RModule and SModule
The Module.RS module gives you ready-made reader/state modules and helpers to integrate existing ReaderT/StateT code.
-- Reader module
data RModule (r :: Type)
instance Module (RModule r) where
newtype ModuleRead (RModule r) = RRead { rRead :: r }
data ModuleState (RModule r) = RState deriving (Generic, NFData)
-- State module
data SModule (s :: Type)
instance Module (SModule s) where
data ModuleRead (SModule s) = SRead
newtype ModuleState (SModule s) = SState { sState :: s }
deriving newtype (Generic, NFData)
Convenience helpers:
-- Reader-like interface
askR :: (Monad m, In' c (RModule r) mods) => EffT' c mods errs m r
asksR :: (Monad m, In' c (RModule r) mods) => (r -> a) -> EffT' c mods errs m a
localR :: (Monad m, In' c (RModule r) mods)
=> (r -> r) -> EffT' c mods errs m a -> EffT' c mods errs m a
-- State-like interface
getS :: (Monad m, In' c (SModule s) mods) => EffT' c mods errs m s
getsS :: (Monad m, In' c (SModule s) mods) => (s -> a) -> EffT' c mods errs m a
putS :: (Monad m, In' c (SModule s) mods) => s -> EffT' c mods errs m ()
modifyS:: (Monad m, In' c (SModule s) mods) => (s -> s) -> EffT' c mods errs m ()
You also get helpers to run and embed modules:
runRModule :: (ConsFDataList c (RModule r : mods), Monad m)
=> r -> EffT' c (RModule r : mods) errs m a
-> EffT' c mods errs m a
runSModule :: (ConsFDataList c (SModule s : mods), Monad m)
=> s -> EffT' c (SModule s : mods) errs m a
-> EffT' c mods errs m (a, s)
runSModule_ :: (ConsFDataList c (SModule s : mods), Monad m)
=> s -> EffT' c (SModule s : mods) errs m a
-> EffT' c mods errs m a
Integrating with ReaderT/StateT (more in Module.RS):
liftReaderT :: forall r mods errs m c a. (Monad m, In' c (RModule r) mods)
=> ReaderT r m a
-> EffT' c mods errs m a
embedReaderT :: forall r mods errs m c a. (Monad m, In' c (RModule r) mods)
=> ReaderT r (EffT' c mods errs m) a
-> EffT' c mods errs m a
RS.Class - MonadReadOnly, MonadReadable, MonadStateful
Control.Monad.RS.Class defines type-class interfaces similar to MonadReader/MonadState, but without functional dependencies, so a single monad can have many readable and stateful values:
class Monad m => MonadReadOnly r m where
query :: m r
queries :: (r -> r') -> m r'
class MonadReadOnly r m => MonadReadable r m where
local :: (r -> r) -> m a -> m a
class Monad m => MonadStateful s m where
get :: m s
put :: s -> m ()
gets :: (s -> a) -> m a
modify :: (s -> s) -> m ()
Instances are provided for EffT' and for monad transformers.
Getting Started - Examples
This section focuses on how to use the core abstractions.
Quick start - algebraic state and errors
A small (made-up) example that:
- stores a
Mapas a state module, - throws a named
ErrorText "Map.keyNotFound"when a key is missing, - and computes an average from a file.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}
module Examples where
import Control.Exception (IOException)
import Control.Monad
import Control.Monad.Effect -- EffT types and combinators
import Module.RS -- RModule / SModule helpers
import System.IO
import qualified Data.Map as M
import qualified Data.Text as T
-- | Wrap a lookup into an algebraic error instead of 'Maybe'.
myLookup
:: (Show k, Ord k, Monad m)
=> k
-> EffT '[SModule (M.Map k v)] '[ErrorText "Map.keyNotFound"] m v
myLookup k =
effMaybeInWith -- this converts a (Maybe v) return value into an algebraic error
(errorText @"Map.keyNotFound" $
" where key = " <> T.pack (show k)) -- using this converter function
(getsS (M.lookup k)) -- :: EffT '[SModule (M.Map k v)] es m (Maybe v)
-- | This effect can run in pure monads like 'Identity' as well as 'IO'.
lookups
:: forall v m. Monad m
=> EffT '[SModule (M.Map T.Text v)] '[ErrorText "Map.keyNotFound"] m (v, v, v)
lookups = do
foo <- myLookup "foo"
bar <- myLookup "bar"
baz <- myLookup "baz"
pure (foo, bar, baz)
parse :: String -> Maybe [Double]
parse = undefined -- parsing logic
computeAverageFromFile
:: FilePath
-> Eff
'[SModule (M.Map T.Text Int)]
[ IOException
, ErrorText "empty-file"
, ErrorText "zero-numbers"
, ErrorText "Map.keyNotFound"
]
Double
computeAverageFromFile fp = do
-- Capture 'IOException' from 'readFile' as an algebraic error.
content <- embedError . liftIOException $ readFile' fp
when (null content) $
effThrowIn (errorText @"empty-file" "file is empty")
-- Turn a 'Maybe' into an ad-hoc algebraic error and immediately handle it.
parsed <- pureMaybeInWith (errorText @"parse-error" "parse error") (parse content)
`effCatch` \(_ :: ErrorText "parse-error") ->
pure [0]
-- Use another effect that requires the same module.
_ <- embedEffT (lookups @Int)
when (null parsed) $
effThrowIn (errorText @"zero-numbers" "zero numbers")
pure $ sum parsed / fromIntegral (length parsed)
You can run such effects in various ways. For example, using the state module runner from Module.RS and runEffT01:
import Control.Monad.Effect
import Module.RS
import qualified Data.Map as M
runCounter
:: Int
-> Eff '[SModule Int] '[ErrorText "zero"] IO ()
-> IO (Either (ErrorText "zero") ((), Int))
runCounter initial action =
runEffT01 (runSModule initial action)
Here:
runSModuleeliminates theSModulefrom the module list while threading the state, andrunEffT01eliminates the remaining error list as anEither.
Embedding and reshaping effects
Sometimes you want to run a smaller effect inside a bigger one; or change modules while keeping the error list, or vice versa. The library provides:
-- Embed a smaller effect into a larger one (modules and/or errors).
embedEffT
:: (SubList c mods mods', SubListEmbed es es', Monad m)
=> EffT' c mods es m a
-> EffT' c mods' es' m a
-- Only change the module list.
embedMods
:: (Monad m, ConsFDataList c mods', SubListEmbed es es, SubList c mods mods')
=> EffT' c mods es m a
-> EffT' c mods' es m a
-- Only change the error list.
embedError
:: (Monad m, SubList c mods mods, SubListEmbed es es')
=> EffT' c mods es m a
-> EffT' c mods es' m a
This is very useful when you have a reusable component that only needs a subset of modules.
Scoped module initialisation
For scoped initialisation and teardown, many projects define higher-level wrappers around the Loadable/withModule pattern. A (simplified) example from a real project:
runApp
:: EffT
'[ RModule ProxyState
, RModule ProxySettings
, PrometheusMan
, LoggingModuleB
]
NoError
IO
()
-> IO ()
runApp app = do
-- Parse options, set up logging and metrics, etc...
opts :: ProxyOptions Unwrapped <- unwrapRecord "Haskell Proxy Server"
case optionsToSettings opts of
Nothing -> putStrLn "Invalid options provided."
Just settings -> do
-- Initialise loggers (implementation-specific).
baseStdLogger <- ...
baseFileLogger <- ...
let logger = baseStdLogger <> baseFileLogger
-- Compose initialisation modules inside Eff, then eliminate back to IO.
runEffT00 $
withLoggerCleanup logger $
(if settings.setPrometheus
then withPrometheus ...
else withNoPrometheusMan) $
do
state <- initializeState settings
runRModule settings $
runRModule state $
app
The exact set of modules (PrometheusMan, LoggingModuleB, ...) is either project-specific or reusable components you can build between projects, but the pattern is always:
build an
EffTstack of modules, run your application logic there, then eliminate modules and errors with the provided runners.
Large application - a bot with many modules
In a larger system you might have many modules and a domain-specific error list. (Taken from a real-world bot application.)
-- The modules loaded into the bot
type Mods =
[ LogDatabase
, AsyncModule
, ProxyWS
, CronTabTickModule
, StatusMonitorModule
, CommandModule
, RecvSentCQ
, MeowActionQueue
, SModule BotConfig
, SModule WholeChat
, SModule OtherData
, MeowConnection
, BotGlobal
, ConnectionManagerModule
, MeowDataDb
, MeowCoreDb
, PrometheusMan
, LoggingModule
]
-- Exceptions that require restarting the bot
type MeowErrs =
'[ ErrorText "recv_connection"
, ErrorText "send_connection"
, ErrorText "meowCoreDb"
]
type MeowT mods m = EffT mods MeowErrs m
type Meow = MeowT Mods IO
The bot initialisation then becomes a composition of withX helpers and run*Module runners:
runBot
:: BotInstance -- ^ Initial bot configuration
-> Meow a -- ^ the bot loop function
-> EffT
'[ BotGlobal
, ConnectionManagerModule
, MeowDataDb
, MeowCoreDb
, PrometheusMan
, LoggingModule
]
'[ErrorText "meowCoreDb"]
IO
()
runBot bot meow = do
-- embed some code into a larger context
botModule <- embedEffT $ botInstanceToModule bot
-- Build additional configuration/state modules...
-- initialise counters, status, etc.
-- not relevant for this example.
embedNoError -- requires the scope inside to have 'NoError' and embeds it to a larger error context
$ effAddLogCat' (LogCat botModule.botId) -- adds a logging category to logs within the scope
$ ( case botRunFlag bot of
RunClient addr port -> void . withClientConnection addr port
RunServer addr port -> withServerConnection addr port
) -- running connection
$ (\app -> do
AllData wc bc od <- embedEffT $ initAllData botconfig
runSModule_ od $ runSModule_ wc $ runSModule_ bc $ app
) -- domain state
$ withMeowActionQueue
$ withRecvSentCQ
$ withModule CommandModuleInitData
$ withModule (StatusMonitorModuleInitData meowStat)
$ maybe id (\init -> withWatchDog init . const) mWatchDogInit
$ withCronTabTick
$ withProxyWS (ProxyWSInitData [(add, ip) | ProxyFlag add ip <- bot.botProxyFlags])
$ withAsyncModule
$ withLogDatabase
$ meow
The details of withMeowActionQueue, withRecvSentCQ, withProxyWS, etc. are application-specific, but the pattern is always:
- each
withXintroduces one or more modules into theEffTstack and arranges their initialModuleRead/ModuleState; and - the final
Meowcomputation runs in a rich module environment, with its error list (MeowErrs) tracking only the domain errors that matter at that layer.
Example - database access
You can also use the type system to enforce that certain low-level errors are handled at the call-site. A typical pattern is to wrap a database action so it:
- requires a specific module to be present; and
- requires a specific error to be in the error list:
runMeowDataDB
:: ( In' c MeowDataDb mods
, InList (ErrorText "meowDataDb") es
)
=> ReaderT SqlBackend IO b
-> EffT' c mods es IO b
Because ErrorText "meowDataDb" is in the error list, callers must either:
- keep that error in their own
es(and propagate it upward), or - explicitly catch and handle it. If they forget, GHC will report a type error.
For example:
-- Forgetting to handle 'ErrorText "meowDataDb"' here leads to a type error.
fetchBlockMessages :: BotId -> ChatId -> ChatBlockSpan -> Meow [ChatMessage]
fetchBlockMessages bid cid span = do
entities <-
runMeowDataDB (selectList [...conditions based on 'bid', 'cid', 'span'...])
`effCatch` \(_ :: ErrorText "meowDataDb") ->
pure []
pure (map entityVal entities)
Here effCatch both catches the database error and removes ErrorText "meowDataDb" from the error list, so the rest of fetchBlockMessages no longer has to account for it.
Selected API Reference
This section is not exhaustive. It highlights key exports; for full details, please consult the Haddock documentation.
Core monad and runners
Eff mods es a,EffT mods es m a,Pure mods es a,ResultT es m a,IO' es m arunEffT,runEffT_,runEffT0,runEffT00,runEffT01,runResultTrunEffTOuter,runEffTOuter',runEffTOuter_- eliminate the outermost module while supplying itsModuleRead/ModuleStaterunEffTIn,runEffTIn',runEffTIn_- eliminate an inner module identified by its typereplaceEffTIn- replace a module with another, using custom conversion functionsNoError,checkNoError,declareNoError,embedNoErrorapplyErrors,applyMods- helpers that exposees/modsto type applications without changing the value
Error machinery
- Types:
Result es a,EList es,SystemError - Named wrappers:
ErrorText s,ErrorValue s v,errorText,errorValue - Throwing:
effThrowIn,effThrow- throw an erroreffThrowEList,effThrowEListIn- throw multiple errors viaEListMonadExcept e mintegration (e.g. viatryAndThrow,tryAndThrowText)
- Catching:
effCatch- catch the first error in the listeffCatchIn- catch a specific error type and remove it from the listeffCatchAll- catch all algebraic errors as anEList es
- Converting errors:
errorToEither,errorToEitherAll,eitherAllToEffecterrorInToEither,errorToMaybe,errorInToMaybe,errorToResultmapError- map one error list into another
- Turning
Either/Maybeinto errors:effEitherWith,effEithereffEitherInWith,effEitherIn,effEitherSystemExceptioneffMaybeWith,effMaybeInWithpureMaybeInWith,pureEitherInWithbaseEitherIn,baseEitherInWith,baseMaybeInWith
IO lifting and exception bridging
These functions help you bridge IO exceptions into algebraic errors:
liftIOException :: MonadIO m => IO a -> EffT' c mods '[IOException] m aliftIOAt :: (Exception e, MonadIO m) => IO a -> EffT' c mods '[e] m aliftIOSafeWith :: (Exception e', MonadIO m) => (e' -> e) -> IO a -> EffT' c mods '[e] m aliftIOText :: MonadIO m => (Text -> Text) -> IO a -> EffT' c mods '[ErrorText s] m aliftIOPrepend :: Text -> IO a -> EffT' c mods '[ErrorText s] IO a
Try/catch style:
effTry,effTryWith- catch exceptions thrown in the base monad and turn them into algebraic errorseffTryIO,effTryIOWith,effTryIOIn,effTryIOInWitheffTryUncaught- catch uncaught exceptions into error liststryAndThrow,tryAndThrowWith,tryAndThrowText- liftIOand rethrow viaMonadExcept
Modules and module helpers
- Core type classes:
Module- definesModuleReadandModuleStateassociated data familiesSystemModule- extendsModulewithModuleEventandModuleInitDataLoadable c mod mods es- provideswithModulefor scoped module initialisation
- System-wide aliases:
SystemRead c mods,SystemState c modsSystemEvent mods,SystemInitData c modsSystemError
- Accessors from
Control.Monad.Effect:queryModule,queriesModule,askModule,asksModulegetModule,getsModule,putModule,modifyModule
withModule (from Control.System) is particularly useful for implementing custom withX helpers that allocate resources, push a module on the stack, run an EffT computation, and then clean up.
Bracket patterns and concurrency
Resource-safe patterns:
maskEffT-maskin the base monad while staying inEffT'generalBracketEffT,generalBracketEffT'bracketEffT,bracketEffT'bracketOnErrorEffT,bracketOnErrorEffT'
Concurrency:
forkEffT- fork anEffTcomputation onto a new threadforkEffTFinally- variants with finalisersasyncEffT,withAsyncEffT,withAsyncEffT'- integrateasyncwithEffTrestoreAsync,restoreAsync_- restore anEffTcomputation from anAsyncresult
RS modules and interfaces
From Module.RS:
- Types:
RModule r,RNamed name rSModule s,SNamed name s
- Running modules:
runRModule,runRModuleInrunSModule,runSModule_,runSModuleIn
- Embedding existing
ReaderT/StateT:liftReaderT,embedReaderT,addReaderT,asReaderTliftStateT,embedStateT,addStateT,asStateT
- Convenience:
askR,asksR,localRgetS,getsS,putS,modifySreadOnly- treat a state module as a read-only module inside a scope
From Control.Monad.RS.Class:
MonadReadOnly r m,MonadReadable r m,MonadStateful s m- reader/state-like APIs without functional dependencies;EffT'has instances for these.
From Control.Monad.Class.Except:
MonadExcept e m- multiple error types per monadErrorText,ErrorValue,errorText,errorValue
Template Haskell utilities
From Module.RS.QQ:
makeRModule,makeRModule_,makeRModule__makeRSModule,makeRSModule_
makeRModule example (simplified):
[makeRModule|MyModule
myRecord1 :: !MyType1
myRecord2 :: MyType2
|]
Generates (conceptually):
data MyModuleinstance Module MyModulewith:data ModuleRead MyModule = MyModuleRead { myRecord1 :: !MyType1, myRecord2 :: MyType2 }data ModuleState MyModule = MyModuleState(plus derivations)
instance SystemModule MyModulewith:data ModuleEvent MyModuledata ModuleInitData MyModule
- runners
runMyModule,runMyModuleIn, etc., and convenient type synonyms forModuleRead/ModuleState.
makeRSModule similarly builds a combined reader/state module from a compact specification, including optional lens generation for fields tagged with Lens.
Performance, style and benchmarks
The core EffT design is very optimisation-friendly:
- modules are stored in a specialised data family
FData, not a linked list; FDatais generated by Template Haskell up to a fixed length (e.g.FData3,FData4, ...) with strict fields; and- GHC can often optimise
EffT-based code down to tight loops, competitive with or better than hand-writtenStateT/ExceptTstacks.
The benchmarks in benchmark/ compare:
EffTwithFList(heterogeneous list),EffTwithFData, andStateTfrommtl.
On typical countdown/state benchmarks:
EffT+FDatais around 25 times faster thanStateTwithout optimisation, and- about as fast as a properly optimised
StateT(-O2 -flate-dmd-anal).
See the SVG charts under benchmark/bench-result-* in the repository for details.
Some Benchmarks
See the benchmark folder for more benchmarks. The benchmarks are copied from heftia, another effect system library, with some modifications.
Countdown -O2
Countdown -O0
Deep Catch -O2
Deep Catch -O0
Local State -O2
Local State -O0
Flags
GHC's type-checker sometimes needs more fuel for large module/error lists. It is recommended to build with:
-fconstraint-solver-iterations=16
or slightly higher when using very deep stacks.
Style and module design
monad-effect does not impose a particular way to structure your modules. You can:
- package a concrete implementation (e.g. Prometheus counter, HTTP manager, database connection pool) directly into a module; or
- use a more “algebraic effects” style, where modules carry handlers for an algebraic effect GADT.
An example of the latter is a Prometheus counter module that carries a handler as read-only state
{-# LANGUAGE DataKinds, TypeFamilies, RequiredTypeArguments #-}
module Module.Prometheus.Counter where
import Control.Monad.Effect
import System.Metrics.Prometheus.Metric.Counter as C
-- | A prometheus counter module that has a name
data PrometheusCounter (name :: k)
-- | Counter effects written in algebraic effect style
data PrometheusCounterEffect a where
AddAndSampleCounter :: Int -> PrometheusCounterEffect CounterSample
AddCounter :: Int -> PrometheusCounterEffect ()
IncCounter :: PrometheusCounterEffect ()
SetCounter :: Int -> PrometheusCounterEffect ()
SampleCounter :: PrometheusCounterEffect CounterSample
-- | The effect handler type for a prometheus counter with given counter name
type PrometheusCounterHandler (name :: k) = forall c mods es m a. (In' c (PrometheusCounter name) mods, MonadIO m) => PrometheusCounterEffect a -> EffT' c mods es m a
-- | The module is declared as a reader module that carries a counter handler
instance Module (PrometheusCounter name) where
newtype ModuleRead (PrometheusCounter name) = PrometheusCounterRead { prometheusCounterHandler :: PrometheusCounterHandler name }
data ModuleState (PrometheusCounter name) = PrometheusCounterState
-- | Specify / interpret a counter effect with given counter name
runPrometheusCounter
:: forall name
-> ( ConsFDataList c (PrometheusCounter name : mods)
, Monad m
)
=> PrometheusCounterHandler name -> EffT' c (PrometheusCounter name ': mods) es m a -> EffT' c mods es m a
runPrometheusCounter name handler = runEffTOuter_ (PrometheusCounterRead @_ @name handler) PrometheusCounterState
{-# INLINE runPrometheusCounter #-}
-- | Carry out a counter effect with given counter name
prometheusCounterEffect :: forall name -> (In' c (PrometheusCounter name) mods, MonadIO m) => PrometheusCounterEffect a -> EffT' c mods es m a
prometheusCounterEffect name eff = do
PrometheusCounterRead handler <- askModule @(PrometheusCounter name)
handler eff
{-# INLINE prometheusCounterEffect #-}
-- | Use a specific counter to carry out a counter effect
useCounter :: Counter -> PrometheusCounterHandler name
useCounter counter IncCounter = liftIO $ C.inc counter
useCounter counter (AddCounter n) = liftIO $ C.add n counter
useCounter counter (SetCounter n) = liftIO $ C.set n counter
useCounter counter (AddAndSampleCounter n) = liftIO $ C.addAndSample n counter
useCounter counter SampleCounter = liftIO $ C.sample counter
{-# INLINE useCounter #-}
-- | A counter handler that does nothing
noCounter :: Monad m => PrometheusCounterEffect a -> EffT mods es m a
noCounter IncCounter = pure ()
noCounter (AddCounter _) = pure ()
noCounter (SetCounter _) = pure ()
noCounter (AddAndSampleCounter _) = pure (CounterSample 0)
noCounter SampleCounter = pure (CounterSample 0)
{-# INLINE noCounter #-}
Documentation changes from previous versions
Compared to earlier versions of the README:
-
The formal definition of
Resulthas been updated to match the code inData.Result(singleRSuccessconstructor, withResult '[] abehaving likea). -
All examples now use the
errorText/errorValuesmart constructors instead of directly constructing using constructorsErrorText/ErrorValue. This avoids the filling kind type parameterErrorText @_ @"..."noise. -
The
myLookup/computeAverageFromFileexample has been synchronised with the code intest/Examples.hsand corrected to useerrorText @"Map.keyNotFound"with a properTextvalue. -
Experimental system orchestration types (
WithSystem,EventLoopSystem, event loops) and theResourcemodule are intentionally not described in detail here, as they may change or be removed in future versions. The focus is onEffT, modules, algebraic errors, and the RS/Except helper classes. -
Minor wording updates were made throughout to reflect that
EffT/Eff(withFData) are the recommended entry points for most users.
For full API details, please refer to the Haddock documentation generated from the source.