Monad transformers in JavaScript
Welcome back. Today we're going to deal with the subject of functional programming, specifically: monadic transformers. It is one of the more advanced topic of the functional programming. IMHO attempt to explain the concept, the why and the how of monadic transformers is a futile one. Instead I will guide you through the evolution of the piece of imperative code, step by step. And at the end we will have fully evolved code using monadic transformers. In each step I'll try to point out advantages of its evolution.
The use-case
Let's start with the use case. We have the database and the webservice (communicating via HTTP protocol). Both of our data storages store our user entity. In database we store these properties of the user: id, user_name, email. In webservice we store these properties: id, twitterId, avatarUri. We have to load data from both our storages, merge them together and serve this merged model to the higher application APIs. Access to both our storages is asynchronous. We're assuming we have following accessors available.
// background: global objects DB and WebService exists
// findUserById :: DB a => a ~> Number -> DbUser
DB.findUserById(1);
//=> Promise({ id: 1, user_name: 'char0n', email: 'vladimir.gorej@gmail.com' });
// findUserById :: WebService a => a ~> Number -> WebServiceUser
WebService.findUserById(1);
// => Promise({ id: 1, twitterId: 'vladimirgorej', avatarUri: 'http://uri.com' })
The imperative world (the ugly world)
Now let's try to rewrite our use-case into imperative code. We all know how to do this right ?
const dbUserP = DB.findUserById(1);
const webServiceUserP = WebService.findUserById(1);
Promise
.all([dbUserP, webServiceUserP])
.then(([dbUser, webServiceUser]) => Object.assign(dbUser, webServiceUser));
//=> {
//=> id: 1, user_name: 'char0n', email: 'vladimir.gorej@gmail.com',
//=> twitterId: 'vladimigorej', avatarUri: 'http://uri.com',
//=> }
This is as far as you get with the imperative code. Actually it's not that ugly by the first look. We are accessing our users and synchronizes them using Promise.all. But there are couple of problems here. We merge our DbUser and WebServiceUser using mutation and Object.assign method. Our DbUser contains properties in snake-case. In JavaScript we'd rather use camel-case. Our merge algorithm is not reusable and thought not testable. Now let's tackle this problem by lifting this imperative code into functional one.
The functional world
We will use pure functions as data transformers. Our transformers will produce new user model called Correspondence model. Transforming DbUser or WebServiceUser will produce the same shape - CorrespondenceUser. Our CorrespondenceUser will have only camel-case properties. We will get rid of the mutations using ramda. Strategy pattern will help up build our API.
const { merge } = require('ramda');
// parseDbUser :: DbUser -> CorrespondenceUser
const parseDbUser = dbUser => ({
id: dbUser.id,
userName: dbUser.user_name,
email: dbUser.email,
twitterId: null,
avatarUri: null,
});
// parseWebServiceUser :: WebServiceUser -> CorrespondenceUser
const parseWebServiceUser = webServiceUser => ({
id: webServiceUser.id,
userName: null,
email: null,
twitterId: webServiceUser.twitterId,
avatarUri: webServiceUser.avatarUri,
});
// toCorrespondence :: CorrespondenceUser d => (a -> d) -> a -> d
const toCorrespondence = curry(
(parseStrategy, dbOrWebServiceUser) => parseStrategy(dbOrWebServiceUser)
);
// fromDb :: DbUser -> CorrespondenceUser
const fromDb = toCorrespondence(parseDbUser);
// fromWebService :: WebServiceUser -> CorrespondenceUser
const fromWebService = toCorrespondence(parseWebServiceUser);
// this is our strategy for merging DbUser and WebServiceUser
// mergeDbAndWebSserviceUser :: (CorrespondenceUser, CorrespondenceUser) => CorrespondenceUser
const mergeDbAndWebServiceUser = (dbUserCm, webServiceUserCm) => merge(dbUserCm, webServiceUserCm);
const dbUserCmP = DB.user.findById(1).then(fromDb);
const webServiceUserCmP = WebService.user.fetchOne(1).then(fromWebService);
Promise
.all([dbUserCmP, webServiceUserCmP])
.then(([dbUserCm, webServiceUserCm]) => mergeDbAndWebServiceUser(dbUserCm, webServiceUserCm));
//=> {
//=> id: 1, userName: 'char0n', email: 'vladimir.gorej@gmail.com',
//=> twitterId: 'vladimigorej', avatarUri: 'http://uri.com',
//=> }
How about that ? That looked better, yes ? We built transformer API that is easy to reason about. We parse the data into correspondence model and then merge it together without mutations. We are no longer concerned by where the data came from because we are always in the realm of the correspondence model. The API is also fully testable. The added benefit of this API is that we can use in anywhere in our application. OK this is all nice and neat, but what happens when one of our storage accessors won't find a user and instead of user object returns null. TypeError is thrown and our application stops working. Let Monads deal will the problem.
The Monad world
What is a Monad ? Well it is type that allows us to perform specific operations and handle specific use-cases. Monad is basically a container that holds a value and allows us to map a function over this value. I will use monet library that gives us implementations of various monadic types. In next section we will be dealing with Either monad specifically. Either monad holds either the value (right side) or the error (left side). And that is exactly what we need for our transformers. Any operations that are performed on Either when it holds the error (left side) are ignored. Detailed explanation of the monads is out of the scope of this article, so I will assume readers basic understanding what monads are and how to use them. So...let's lift our transformers into monadic context.
const { Either } = require('monet');
// liftEither :: (a -> b) -> a -> Either b
const liftEither = (fn) => (model) => {
try {
return Either.Right(fn(model));
} catch (e) {
return Either.Left(e);
}
}
// safeFromDb :: DbUser -> Either CorrespondenceUser
const safeFromDb = liftEither(toCorrespondence(parseDbUser));
// safeFromWebService :: WebServiceUser -> Either CorrespondenceUser
const safeFromWebService = liftEither(toCorrespondence(parseWebServiceUser));
Our transformer functions are now safe and don't throw any errors. Instead of correspondence model they return correspondence model wrapped into Either. Now we need new version of merge that can merge Eithers instead of data.
const { curry, partial } = require('ramda');
// mergeM :: ((a, b) -> c) -> Either b -> a -> Either c
const mergeM = curry((strategyFn, anotherEither, correspodenceModel) =>
anotherEither.map(partial(strategyFn, [correspondenceModel]))
});
Now let's evolve our code some more using this new machinery. Don't forget that our promise now contains Either instead of value. At the end of the Promise chain we will have to unwrap the value from Either.
const { cata } = require('ramda-adjunct');
const dbUserCmP = DB.findUserById(1).then(safeFromDb);
const webServiceUserCmP = WebService.findUserById(1).then(safeFromWebService);
const resolveP = Promise.resolve.bind(Promise);
const rejectP = Promise.reject.bind(Promise);
Promise
.all([dbUserCmP, webServiceUserCmP])
.then(([dbUserCm, webServiceUserCm]) =>
dbUserCm.chain(mergeM(mergeDbAndWebServiceUser, webServiceUserCm))
)
.then(cata(rejectP, resolveP));
Yes I hear you...that looks more complicated that the original imperative code. It is caused by the Promise <-> Monad paradox. Basically Promise is an implementation of a continuation monad. But IMHO not very fortunate implementation. Promise is eager and doesn't have monadic interface. What if we had a monadic type that can handle asynchronous values and is also lazy ?
The future world
Actually there is a monad that can replace promises entirely. It is usually called Future or Task. My favorite implementation of Future monad is fluture. Let's try to use fluture instead of promises and evolve our code into the future world ;]
const { Future } = require('fluture');
Future
.both(
Future.encaseP(DB.findUserById, 1).map(safeFromDb),
Future.encaseP(WebService.findUserById, 1).map(safeFromWebService)
)
.map(([dbUserCm, webServiceUserCm]) =>
dbUserCm.chain(mergeM(mergeDbAndWebServiceUser, webServiceUserCm))
))
.chain(cata(Future.reject, Future.resolve)) // natural transformation
.promise()
Declarative, clean and the code is lazy. Lazy means it is not executed until promise() method is called. Just for clarity, Future.encaseP encases a Promise generating function with a single argument. We now have values or errors wrapped inside Either that is wrapped inside Future. Although we have to use natural transformations to fold the inner Either into Future to get the value out. I guest you now see the problem and the solution your self. What if we had a monad that would act as a Future and would know that he has Either monad wrapped inside it ?
The Monad transformers world
Yes you guessed it. Monad transformers are just an abstraction for natural transformations. Let's lift our code snippet into monad transformers context.
const { apply } = require('ramda');
const FutureTEither = require('monad-t/lib/FlutureTMonetEither');
FutureTEither
.both(
FutureTEither.encaseP(DB.findUserById, 1).chainEither(safeFromDb),
FutureTEither.encaseP(WebService.findUserById, 1).chainEither(safeFromWebService)
)
.map(apply(mergeDbAndWebServiceUser))
.promise()
Now for the most important question. How does it even work ? Well all monad transformers and again only Monads. FutureTEither does exactly what it says in it's name. It is a Future that transforms Either inside it. It is a type that is aware of the fact that is wraps Future that has Either locked inside it. When we map over the FutureTEither, under the hood firstly the Future is mapped and if it is not rejected Future, then the Either is mapped only and if only Either is right side. FutureTEither know that when it has Either left side locked inside it, running it (e.g. calling promise()) means that the inner Either is transformed into Future's rejection.
All of these laws of natural transformations are implemented in monad-t library. Browse the source code and documentation to fully understand what is going on under the hood.
For monad transformers to be practical, they usually have to mach the API of it's run. run is a designation we use for an inner monad we are currently transforming. So in our case run for FutureTEither is Future monad that has Either locked inside it. When your monad transformer receives a monad that he doesn't recognize, it can still assume that the passed monad will be compatible with fantasy land monad specification. Which means is will have ap, map and chain methods. By this assumption we can always remap these methods into run. Any other custom API (non fantasy land) of the inner monad has to be manually re-implemented into outer transformer monad to match it. These laws allows us to stack/compose multiple levels of monad transformers. Observe the monad stacking in action in the following code snippet.
// let's assume that Monad is a fantasy-land monad of unknown type
EitherT(EitherT(Monad.of(1))) // 1
.map(a => a + 1) // 2
.chain(a => EitherT(EitherT(Monad.of(a + 1)))) // 3
.ap(EitherT(EitherT(Monad.of(1))).map(a => b => a + b)) // 4
.run // 5
.run // Monad(4) // 6
This is kind of mind-bending but surely still easier to comprehend that Y Combinator (joke) ;]. Let's break it down.
Line 1: Reading the line from right to left, we are creating instance of Monad<Number> and passing it into EitherT. EitherT doesn't recognize the Monad<Number> but assumes that it is fantasy-land compatible and create the EitherT<Monad<Number>> instance with run set to Monad<Number> instance. The EitherT<Monad<Number>> instance is again passed into another EitherT call. Again EitherT doesn't recognize the EitherT<Monad<Number>> instance and assumes that the EitherT<Monad<Number>> instance is fantasy-land compatible monad. Then it creates yet another EitherT<EitherT<Monad<Number>>> instance. So...after first line is executed we have three level deep stack of monads.
Line 2: We map the most outer EitherT instance and the map is propagated all the way down to the original Monad instance.
Line 3: chain method expects a function that returns monad instance of the same type. Operation is again propagated all the way down.
Line 4: ap method expects a monad instance of the same type with function locked inside it. Again propagation all the way down.
Line 5, 6: now that our operations were successfully performed lets unwrap the inner-most Monad instance.
Note: as of today this stacking is possible using code from master branch of monad-t. It will be available via npm in next monad-t@0.3.0 release.
That concludes our business for today. Again I end my article with the usual axiom: Define your code-base as pure functions and lift them only if needed. And compose, compose, compose...
Closing note: I know that the subject of monad transformers is not an easy subject to absorb, so please if you have any questions do not hesitate to leave me a comment a I'll try to do my best to help you.
I may have to brush up on applicatives first too.
I've loving this man. But, I'm gonna have to wake up early soon and get a big cup of coffee going to fully grok this. I gotta be honest, I love that I need coffee to get it :) Thanks for the contribution to the fp community!
Gotcha, nice explanation. I like the initial data mapped into `CorrespondenceUser` and I should use your monad transformers. Does this mean that you have to implement `FutureTEither.both` and `FutureTEither.encaseP` to match `Fluture` API yourself?