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.logcould 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.logwould 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
Top comments (0)