5

I'm trying to cook up a simple example using IO and Maybe monads. The program reads a node from the DOM and writes some innerHTML to it.

What I'm hung up on is the combination of IO and Maybe, e.g. IO (Maybe NodeList).

How do I short circuit or throw an error with this setup?

I could use getOrElse to extract a value or set a default value, but setting the default value to just an empty array doesn't help anything.

import R from 'ramda';
import { IO, Maybe } from 'ramda-fantasy';
const Just    = Maybe.Just;
const Nothing = Maybe.Nothing;

// $ :: String -> Maybe NodeList
const $ = (selector) => {
  const res = document.querySelectorAll(selector);
  return res.length ? Just(res) : Nothing();
}

// getOrElse :: Monad m => m a -> a -> m a
var getOrElse = R.curry(function(val, m) {
    return m.getOrElse(val);
});


// read :: String -> IO (Maybe NodeList)
const read = selector => 
  IO(() => $(selector));

// write :: String -> DOMNode -> IO
const write = text => 
                  (domNode) => 
                    IO(() => domNode.innerHTML = text);

const prog = read('#app')
                  // What goes here? How do I short circuit or error?
                  .map(R.head)
                  .chain(write('Hello world'));

prog.runIO();

https://www.webpackbin.com/bins/-Kh2ghQd99-ljiPys8Bd

Will M
  • 2,135
  • 4
  • 22
  • 32
  • Here's a version with Either instead of Maybe: https://www.webpackbin.com/bins/-Kh36r7GT7TR1ksMcWMM Maybe that's the way to go... – Will M Apr 06 '17 at 16:41

2 Answers2

3

You could try writing an EitherIO monad transformer. Monad transformers allow you to combine the effects of two monads into a single monad. They can be written in a generic way such that we can create dynamic combinations of monads as needed, but here I'm just going to demonstrate a static coupling of Either and IO.

First we need a way to go from IO (Either e a) to EitherIO e a and a way to go from EitherIO e a to IO (Either e a)

EitherIO :: IO (Either e a) -> EitherIO e a
runEitherIO :: EitherIO e a -> IO (Either e a)

And we'll need a couple helper functions for taking other flat types to our nested monad

EitherIO.liftEither :: Either e a -> EitherIO e a
EitherIO.liftIO :: IO a -> EitherIO e a

To conform with fantasy land, our new EitherIO monad has a chain method and of function and obeys the monad laws. For your convenience, I also implemented the functor interface with the map method.

EitherIO.js

import { IO, Either } from 'ramda-fantasy'
const { Left, Right, either } = Either

// type EitherIO e a = IO (Either e a)
export const EitherIO = runEitherIO => ({
  // runEitherIO :: IO (Either e a)
  runEitherIO, 
  // map :: EitherIO e a => (a -> b) -> EitherIO e b
  map: f =>
    EitherIO(runEitherIO.map(m => m.map(f))),
  // chain :: EitherIO e a => (a -> EitherIO e b) -> EitherIO e b
  chain: f =>
    EitherIO(runEitherIO.chain(
      either (x => IO.of(Left(x)), (x => f(x).runEitherIO))))
})

// of :: a -> EitherIO e a
EitherIO.of = x => EitherIO(IO.of(Right.of(x)))

// liftEither :: Either e a -> EitherIO e a
export const liftEither = m => EitherIO(IO.of(m))

// liftIO :: IO a -> EitherIO e a
export const liftIO = m => EitherIO(m.map(Right))

// runEitherIO :: EitherIO e a -> IO (Either e a)
export const runEitherIO = m => m.runEitherIO

Adapting your program to use EitherIO

What's nice about this is your read and write functions are fine as they are - nothing in your program needs to change except for how we structure the calls in prog

import { compose } from 'ramda'
import { IO, Either } from 'ramda-fantasy'
const { Left, Right, either } = Either
import { EitherIO, liftEither, liftIO } from './EitherIO'

// ...

// prog :: IO (Either Error String)
const prog =
  EitherIO(read('#app'))
    .chain(compose(liftIO, write('Hello world')))
    .runEitherIO

either (throwError, console.log) (prog.runIO())

Additional explanation

// prog :: IO (Either Error String)
const prog =
  // read already returns IO (Either String DomNode)
  // so we can plug it directly into EitherIO to work with our new type
  EitherIO(read('#app'))
    // write only returns IO (), so we have to use liftIO to return the correct EitherIO type that .chain is expecting
    .chain(compose(liftIO, write('Hello world')))
    // we don't care that EitherIO was used to do the hard work
    // unwrap the EitherIO and just return (IO Either)
    .runEitherIO

// this actually runs the program and clearly shows the fork
// if prog.runIO() causes an error, it will throw
// otherwise it will output any IO to the console
either (throwError, console.log) (prog.runIO())

Checking for errors

Go ahead and change '#app' to some non-matching selector (eg) '#foo'. Re-run the program and you'll see the appropriate error barfed into the console

Error: Could not find DOMNode

Runnable demo

You made it this far. Here's a runnable demo as your reward: https://www.webpackbin.com/bins/-Kh5NqerKrROGRiRkkoA



Generic transform using EitherT

A monad transformer takes a monad as an argument and creates a new monad. In this case, EitherT will take some monad M and create a monad that effectively behaves has M (Either e a).

So now we have some way to create new monads

// EitherIO :: IO (Either e a) -> EitherIO e a
const EitherIO = EitherT (IO)

And again we have functions to lifting the flat types into our nested type

EitherIO.liftEither :: Either e a -> EitherIO e a
EitherIO.liftIO :: IO a -> EitherIO e a

Lastly a custom run function that makes it easier to handle our nested IO (Either e a) type - notice, one layer of abstraction (IO) is removed so we only have to think about the Either

runEitherIO :: EitherIO e a -> Either e a

EitherT

is the bread and butter - the primary difference you see here is that EitherT accepts a monad M as an input and creates/returns a new Monad type

// EitherT.js
import { Either } from 'ramda-fantasy'
const { Left, Right, either } = Either

export const EitherT = M => {
   const Monad = runEitherT => ({
     runEitherT,
     chain: f =>
       Monad(runEitherT.chain(either (x => M.of(Left(x)),
                                      x => f(x).runEitherT)))
   })
   Monad.of = x => Monad(M.of(Right(x)))
   return Monad
}

export const runEitherT = m => m.runEitherT

EitherIO

can now be implemented in terms of EitherT – a dramatically simplified implementation

import { IO, Either } from 'ramda-fantasy'
import { EitherT, runEitherT } from './EitherT'

export const EitherIO = EitherT (IO)

// liftEither :: Either e a -> EitherIO e a
export const liftEither = m => EitherIO(IO.of(m))

// liftIO :: IO a -> EitherIO e a
export const liftIO = m => EitherIO(m.map(Either.Right))

// runEitherIO :: EitherIO e a -> Either e a
export const runEitherIO = m => runEitherT(m).runIO()

Updates to our program

import { EitherIO, liftEither, liftIO, runEitherIO } from './EitherIO'

// ...

// prog :: () -> Either Error String
const prog = () =>
  runEitherIO(EitherIO(read('#app'))
    .chain(R.compose(liftIO, write('Hello world'))))

either (throwError, console.log) (prog())

Runnable demo using EitherT

Here's the runnable code using EitherT: https://www.webpackbin.com/bins/-Kh8S2NZ8ufBStUSK1EU

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thank you for the thorough explanation! I'll read and reread until I understand. – Will M Apr 07 '17 at 13:44
  • You're very welcome. Monad transformers are a new study for me and it's taken a good week of reading to start to get a good grasp on them. I think I was originally making them more complicated than necessary! I wanted to cover writing a generic transformer `EitherT` that would let you do something like `const EitherIO = EitherT (IO)`. This let's you create dynamic nesting of monads instead of the static coupling I wrote in my answer. I'll try to get around to that this weekend if I have time ^_^ – Mulan Apr 07 '17 at 15:48
  • I found this in my own research https://github.com/mattbierner/akh-either Not sure if it'll be helpful – Will M Apr 07 '17 at 15:53
  • the akh repositories are very useful and thorough but pretty highly engineered. You'll have to chase around several files to see how things fit together, but it's all very well written. – Mulan Apr 07 '17 at 15:55
  • I adapted some of the code found in akh's EitherT transformer and implemented a simplified version for you here in an update to this answer. Monad transformers are quite fun! – Mulan Apr 07 '17 at 20:40
  • @naomik why do you wrap the result of the chain function back in the Monad in the EitherT implementation? I would expect that for map but not for chain... THanks – akaphenom Jul 28 '17 at 10:52
  • @akaphenom the type of `chain` is `m a -> (a -> m b) -> m b` so if `m` is `EitherT`, `chain` must return an `EitherT`. The *trick* here is we're also expecting the user-supplied function to return an `EitherT`, so in that case we would end up with `EitherT`, wrapped around another `EitherT`, which is why we call `.runEitherT` to unwrap the additional level of nesting. – Mulan Jul 28 '17 at 16:20
2

You could create a helper function that will conditionally chain with another IO producing function if the given predicate returns true. If it returns false it will produce an IO ().

// (a → Boolean) → (a → IO ()) → a → IO ()
const ioWhen = curry((pred, ioFn, val) =>
  pred(val) ? ioFn(val) : IO(() => void 0))

const $ = document.querySelector.bind(document)

const read = selector => 
  IO(() => $(selector))

const write = text => domNode =>
  IO(() => domNode.innerHTML = text)

const prog = read('#app').chain(
  ioWhen(node => node != null, write('Hello world'))
)

prog.runIO();
Scott Christopher
  • 6,458
  • 23
  • 26