avail
avail
is a companion to monad transformers that allows you to impose effect constraints on concrete monads. Specifically, instead of writing
myApp :: (MonadWriter Log m, MonadState Store m, MonadReader Env m) => m ()
it allows you to write
myApp :: Effs '[MonadWriter Log, MonadState Store, MonadReader Env] => App ()
where App
is a specific, concrete monad stack.
Introduction
Current effects libraries all have one principle of effect restriction: an effect can be used in a monad if it can be interpreted in terms of the monad. This works well with a polymorphic monad type, but a polymorphic type is unfriendly to compiler optimization. In contrast, a concrete monad can be easily optimized, but if we fix a monad that supplies all the effects we need, we can no longer restrict what effects each function can use.
avail
solves this problem with the phantom constraint pattern. We use a newtype wrapper M
to screen out the user from directly manipulating the underlying monad, and performing any operation in a typeclass (for example, MonadIO
) requires the corresponding phantom Eff
constraint (in this case. Eff MonadIO
) to be in scope. In other words, one can perform operations of a class only if:
- The class is implemented for the monad, and
- The effect constraint
Eff e
is available in the context.
The second requirement decouples the availability of effects from the monad implementation. At last, we use a function runM
to clear the constraints and restore the underlying monad. A typical example looks like this:
import Avail
import Control.Monad.Reader
import Control.Monad.State
type App = M (ReaderT Int (State Bool))
testParity :: Effs '[MonadReader Int, MonadState Bool] => App ()
testParity = do
num <- ask
put (even num)
example :: IO ()
example = do
print $ runM @'[MonadReader Int, MonadState Bool] testParity
& (`runReaderT` 2)
& (`execState` False)
print $ runM @'[MonadReader Int, MonadState Bool] testParity
& (`runReaderT` 3)
& (`execState` False)
Through microbenchmarks and tests in some applications, the performance of using avail
is at least on par with, and often better than, using the polymorphic counterparts.
Making avail
work with new typeclasses
avail
already comes with support of all mtl
, exceptions
, unliftio
, monad-control
and capability
typeclasses. To add support for your own typeclass, for example:
class MonadOvO m where
...
You can use the Template Haskell utilities in the Avail.Derive
module.
import Avail.Derive
avail [t| MonadOvO |]
There may be other more complicated cases, such as dependencies between classes and multi-param classes:
class (MonadOvO m, MonadQwQ m) => MonadUwU r m where
...
avail
gets you covered:
import Avail.Derive
with1 \r -> avail'
[ [t| MonadOvO |]
, [t| MonadQwQ |]
] [t| MonadUwU $r |]
Limitations
-
Running effects:
Because effect constraints in avail
are detached from the monad structure, they cannot be run on a one-by-one basis. Practically, one can only run all effects and obtain the underlying concrete monad at once via runM
. This means there is no exact equivalent to runReaderT
, runExceptT
etc on the M
monad.
If your application can entirely run on a single transformer stack (in particular, ReaderT IO
, but also other transformer stacks), this is a non-issue because there will be no need to run effects one-by-one. For some other scenarios, there are some solutions that may be used solve this issue:
local
is an almost identical substitute to runReaderT
without destructing the monad.
- Similarly,
tryError
is a substitute to runExceptT
.
- To simulate
runStateT
, simply set the value before the action and get the value after it.
listen
is a very close analog to runWriterT
.
Where is availability
?
The old availability
is abandoned due to its attempt at addressing two problems at once (capability and availability management), and may introduce confusion to new users on the choice between capability
and availability
. The new avail
library focuses on availability management and acts not as a standalone effects library, but an alternative effect management layer that is used together with mtl
or capability
(which also reduces preformance overhead).
The old availability
can be found at re-xyr/availability-old
.