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();
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