DEV Community

Cover image for How monads encapsulate side effects
Mike Solomon
Mike Solomon

Posted on

How monads encapsulate side effects

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. 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 that console.log would go south like this.

These two concepts have distinct representations.

  1. 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.

  2. 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.

  1. nominal: Something happened in the outside world.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode
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")
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)