How monads encapsulate side effects
Mike Solomon
Posted on March 26, 2021
At Meeshkan, I'm always thrilled to welcome new hires, and I'm extra-super-thrilled to welcome our two new PureScript developers to the team: Vincent Orr and Muse Mekuria. Their stories span France, England, Ethiopia and America, and their programming careers span many languages and projects, but what unites them at this team at this moment is our PureScript code base (among other things!).
Vince's technical interview resulted in an article on modeling transactional logic with types. For Muse's hire, I'd like to write an article about modeling effects with monads based on a discussion we had recently. The goal is to explain what Effect
in PureScript (or IO
in Haskell) is and why it is a monad.
Kleisli arrows
Monads hang out at the end of Kleisli arrows. A Kleisli arrow is a function from a -> b
that wraps b
in a something extra called m
. That's a monad. Let's see some examples of Kleisli arrows in Haskell or PureScript:
-- A Kleisli arrow (a -> m a) where m = (r -> a)
env :: forall r. a -> (r -> a)
env a _ = a
-- A Kleisli arrow (a -> m a) where m = Maybe
just :: a -> Maybe a
just = Just
-- Another Kleisli arrow (a -> m a) where m = Maybe
nothing :: a -> Maybe a
nothing _ = Nothing
Monads multiply. For example, in the case of Maybe, if there are n functions from a -> b
, there are 2n functions from a -> Maybe b
because there are two branches in Maybe
- Just a
and Nothing
.
What do we mean when we say "effect"?
When we talk about effects, we are talking about two interrelated but distinct concepts:
Effect signals that the outside world is somehow changed as a result of our program doing something, and some of that change cannot potentially be propagated back into the program. For example, when I write to
console.log
, the luminosity of the screen, its connection with my retina, and the effect the information has on my brain exists in the world and cannot propagate back into the program.Effect signals something that may go wrong. Because we are venturing into the outside world, we simply don't know how things will go. For example,
console.log
could display something so heinous that, when going from my retina to my brain, it resulted in me throwing my computer out the window, causing the program to terminate. There is no way the program could have known thatconsole.log
would go south like this.
These two concepts have distinct representations.
A monad that signals "this changes the outside world" is nominal in nature. It is an indication via the type system that a change happened. When we see
Effect Unit
, we know that by executing that monad, something will change. It is purely on the level of documentation.A monad that signals "something may go wrong" is actionable in nature. It is an indication via the type system that things may blow up and you can choose to deal with it or not.
Effects as Kleisli arrows
Effect, in the way we typically talk about it (and in the way IO
in Haskell and Effect
in PureScript work) are both nominal and actionable.
- nominal: Something happened in the outside world.
- actionable: That thing may have gone South, and you can do something about it.
So if Effect
works this way, how can we model it? Using Maybe
above is a good start: it has everything we want:
data Maybe a = Just a | Nothing
The Just
branch is nominal and the Nothing
branch is actionable. If we get a Just
, we can breathe a sigh of relief, and if we get a Nothing
, we need to do some sort of cleanup and/or quit the program.
So how is Effect
like Maybe
? In PureScript, Effect
is a computation that happens in JavaScript. The success branch is the result of the computation, and the failure branch is an Error
.
There's one small wrinkle, though - we need to "wrap" a value in our effect (ie Effect Unit
, Effect Int
, etc). Meaning that it needs some context in the success case, just like Just
is the context for a
in Just a
. There are various ways we can do this wrapping, and the one PureScript chooses is to wrap the result in a thunk, or function with 0 arguments. That guarantees that the execution of the code will be delayed until you call myThunk()
.
DIY effects
Now that we know how effects work, let's roll our own! We'll build them from the ground up, meaning no libraries - in just a few lines of code, we'll have our own effect system.
Nominal
First, we'll do the nominal bit. This acknowledges that a side effect happened (ie writing to the console) without any attempt to handle the case where logging errors out.
// Main.js
exports.bindEffect = function(ma) {
return function(aToMb) {
return function () {
return aToMb(ma())();
}
}
}
exports.log = function(s) {
return function() {
console.log(s);
}
}
module Main where
class Bind m where
bind :: forall a b. m a -> (a -> m b) -> m b
data Unit = Unit
data Effect a
foreign import bindEffect :: forall a b. Effect a -> (a -> Effect b) -> Effect b
foreign import log :: String -> Effect Unit
instance bindEffect_ :: Bind Effect where
bind = bindEffect
main :: Effect Unit
main = bind (log "hello") (\_ -> log "world")
Actionable
Now let's add a bit more code to deal with errors. naughty
provokes an error and nice
catches it.
exports.evil = new Error("I'm naughty, deal with it.");
exports.catchErrorEffect = function(ma) {
return function(eToMa) {
return function() {
try {
return ma();
} catch (e) {
return eToMa(e)();
}
}
}
}
exports.throwErrorEffect = function(e) {
return function() {
throw e
}
}
exports.bindEffect = function(ma) {
return function(aToMb) {
return function () {
return aToMb(ma())();
}
}
}
exports.log = function(s) {
return function() {
console.log(s);
}
}
module Main where
data Unit = Unit
data Effect a
data Error
foreign import log :: String -> Effect Unit
class Bind m where
bind :: forall a b. m a -> (a -> m b) -> m b
foreign import bindEffect :: forall a b. Effect a -> (a -> Effect b) -> Effect b
instance bindEffect_ :: Bind Effect where
bind = bindEffect
class MonadThrow e m | m -> e where
throwError :: forall a. e -> m a
foreign import throwErrorEffect :: forall a. Error -> Effect a
instance throwErrorEffect_ :: MonadThrow Error Effect where
throwError = throwErrorEffect
class (MonadThrow e m) <= MonadError e m | m -> e where
catchError :: forall a. m a -> (e -> m a) -> m a
foreign import catchErrorEffect :: forall a. Effect a -> (Error -> Effect a) -> Effect a
instance catchErrorEffect_ :: MonadError Error Effect where
catchError = catchErrorEffect
foreign import evil :: Error
naughty :: Effect Unit
naughty = bind
(log "hello")
(\_ -> bind (throwError evil) \_ -> log "world")
nice :: Effect Unit
nice = bind
(log "hello")
(\_ ->
bind
(catchError (throwError evil) (\_ -> log "dodged that bullet!"))
(\_ -> log "world"))
main :: Effect Unit
main = nice
--main :: Effect Unit
--main = naughty
Posted on March 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 29, 2024