Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Synopsis
In brief
Bluefin is an effect system which allows you, though value-level handles, to freely mix a variety of effects including
- Bluefin.EarlyReturn, for early return
- Bluefin.Exception, for exceptions
- Bluefin.IO, for I/O
- Bluefin.State, for mutable state
- Bluefin.Stream, for streams
Introduction
Bluefin is a Haskell effect system with a new style of API.
It is distinct from prior effect systems because effects are
accessed explicitly through value-level handles which occur as
arguments to effectful operations. Handles (such as
State
handles, which allow access to mutable
state) are introduced by handlers (such as
evalState
, which sets the initial state).
Here's an example where a mutable state effect handle, sn
, is
introduced by its handler, evalState
.
-- Ifn < 10
then add 10 to it, otherwise -- return it unchanged example1 :: Int -> Int example1 n =runPureEff
$ -- Create a new state handle, sn, and -- initialize the value of the state to nevalState
n $ \sn -> do n' <-get
sn when (n' < 10) $modify
sn (+ 10) get sn
>>> example1 5 15 >>> example1 12 12
The handle sn
is used in much the same way as an
STRef
or IORef
.
Multiple effects of the same type
A benefit of value-level effect handles is that it's simple
to have multiple effects of the same type in scope at the same
time. It's easy to disambiguate them because they are distinct
values! It is not simple with existing effect systems because
they require the disambiguation to occur at the type level.
Here is an example with two mutable Int
state effects in
scope.
-- Compare two values and add 10 -- to the smaller example2 :: (Int, Int) -> (Int, Int) example2 (m, n) =runPureEff
$evalState
m $ \sm -> do evalState n $ \sn -> do do n' <-get
sn m' <- get sm if n' < m' thenmodify
sn (+ 10) else modify sm (+ 10) n' <- get sn m' <- get sm pure (n', m')
>>> example2 (5, 10) (15, 10) >>> example2 (30, 3) (30, 13)
Effect scoping
Bluefin's use of the type system is very similar to
ST
: it ensures that a handle can never escape
the scope of its handler. That is, once the handler has
finished running there is no way you can use the handle
anymore.
Comparison to other effect systems
Everything except effectful
The design of Bluefin is strongly inspired by and based on effectful. All the points in effectful's comparison of itself to other effect systems apply to Bluefin too.
effectful
The major difference between Bluefin and effectful is that in
Bluefin effects are represented as value-level handles whereas
in effectful they are represented only at the type level.
effectful could be described as "a well-typed implementation of
the ReaderT
IO
pattern", and Bluefin could be described as
a well-typed implementation of something even simpler: "the
functions-that-return-IO
pattern". The aim of the Bluefin
style of value-level effect tracking is to make it even easier
to mix effects, especially effects of the same type. Only time
will tell which approach is preferable in practice.
"Why not just implement Bluefin as an alternative API on top of effectful?"
It would be great to share code between the two projects! But
there are two Bluefin features that I don't know to implement
in terms of effectful: Coroutine
s and
Compound
effects.
Implementation
Bluefin has a similar implementation style to effectful.
Eff
is an opaque wrapper around IO
,
State
is an opaque wrapper around
IORef
, and throw
throws an
actual IO
exception. Coroutine
, which
doesn't exist in effectful, is implemented simply as a
function.
newtypeEff
(es ::Effects
) a =UnsafeMkEff
(IO a) newtypeState
s (st :: Effects) =UnsafeMkState
(IORef s) newtypeCoroutine
a b (s :: Effects) =UnsafeMkCoroutine
(a -> IO b)
The type parameters of kind Effects
are phantom
type parameters which track which effects can be used in an
operation. Bluefin uses them to ensure that effects cannot
escape the scope of their handler, in the same way that the
type parameter to the ST
monad ensures that
mutable state references cannot escape
runST
. When the type system indicates that
there are no unhandled effects it is safe to run the underlying
IO
action using unsafePerformIO
, which is
the approach taken to implement runPureEff
.
Tips
- Use
NoMonoLocalBinds
andNoMonomorphismRestriction
for better type inference. - Writing a handler often requires an explicit type signature.
Example
countPositivesNegatives :: [Int] -> String countPositivesNegatives is =runPureEff
$evalState
(0 :: Int) $ \positives -> do r <-try
$ \ex -> evalState (0 :: Int) $ \negatives -> do for_ is $ \i -> do case compare i 0 of GT ->modify
positives (+ 1) EQ -> throw ex () LT -> modify negatives (+ 1) p <-get
positives n <- get negatives pure $ "Positives: " ++ show p ++ ", negatives " ++ show n case r of Right r' -> pure r' Left () -> do p <- get positives pure $ "We saw a zero, but before that there were " ++ show p ++ " positives"