4

I'm having a hard time understanding monad transformers, partly because most examples and explanations use Haskell.

Could anyone give an example of creating a transformer to merge a Future and an Either monad in Javascript and how it can be used.

If you can use the ramda-fantasy implementation of these monads it would be even better.

Marcelo Lazaroni
  • 9,819
  • 3
  • 35
  • 41
  • What would be the end result of that merger? Afaict monad transformers are functions that take a monad and return a monad satisfying a few rules: https://en.wikipedia.org/wiki/Monad_transformer#Definition – Alex Pánek Mar 14 '17 at 10:47
  • I am using an Either monad to handle validation inside a Future monad handling async flow. Handling one monad inside another is not very clean and chaining the Future monad gets specially tricky. I read monad transformers could give me a cleaner API composed of these two monads. Is that right? If so, how does that look like in Javascript? – Marcelo Lazaroni Mar 14 '17 at 10:57
  • @AlexPánek this wikipedia article is an example of an explanation that is barely intelligible to a JavaScript developer that does not know Haskell. I am looking for an explanation using plain JavaScript code. – Marcelo Lazaroni Mar 14 '17 at 11:01
  • Could you outline how you tried implementing this so far? – Alex Pánek Mar 14 '17 at 11:52
  • @AlexPánek I believe the question was pretty straight forward. It's not about my code in specific, it's about what monad transformers are and how do they work in JavaScript. – Marcelo Lazaroni Mar 14 '17 at 12:04
  • 1
    This question is too broad. Anyway, AFAIK, monad transformers indeed facilitate monad composition. For example, they can help you to avoid deeply nested chain calls. A Javascript implementation is available on [Fantasy Land](https://github.com/fantasyland/fantasy-eithers/blob/master/src/either.js) and in this SO [question](http://stackoverflow.com/questions/39598029/is-this-a-valid-monad-transformer-in-javascript). Please note that not every monad composition yields a monad, i.e. you might lose some of the monad laws. –  Mar 14 '17 at 14:15
  • @MarceloLazaroni I never intended to say it wasn't. But I wouldn't know enough about the topic as a whole to be able to give an answer, that is why I asked what you have so far. It might help *me* understand what you are trying to achieve better. :) – Alex Pánek Mar 15 '17 at 12:30
  • this is an old question but for anyone revisiting it, here is a video with this exact question answered in depth! https://www.youtube.com/watch?v=mVL1_M1yFo4&list=PL5pTdt4hRGgqk7OPtNEROvUV_mIt34rTG – Alfred Young Jul 09 '19 at 03:50

1 Answers1

19

Rules first

First we have the Natural Transformation Law

  • Some functor F of a, mapped with function f, yields F of b, then naturally transformed, yields some functor G of b.
  • Some functor F of a, naturally transformed yields some functor G of a, then mapped with some function f, yields G of b

Choosing either path (map first, transform second, or transform first, map second) will lead to the same end result, G of b.

natural transformation law

nt(x.map(f)) == nt(x).map(f)

Getting real

Ok, now let's do a practical example. I'm gonna explain the code bit-by-bit and then I'll have a complete runnable example at the very end.

First we'll implement Either (using Left and Right)

const Left = x => ({
  map: f => Left(x),
  fold: (f,_) => f(x)
})

const Right = x => ({
  map: f => Right(f(x)),
  fold: (_,f) => f(x),
})

Then we'll implement Task

const Task = fork => ({
  fork,
  // "chain" could be called "bind" or "flatMap", name doesn't matter
  chain: f =>
    Task((reject, resolve) =>
      fork(reject,
           x => f(x).fork(reject, resolve)))
})
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))

Now let's start defining some pieces of a theoretical program. We'll have a database of users where each user has a bff (best friend forever). We'll also define a simple Db.find function that returns a Task of looking up a user in our database. This is similar to any database library that returns a Promise.

// fake database
const data = {
  "1": {id: 1, name: 'bob', bff: 2},
  "2": {id: 2, name: 'alice', bff: 1}
}

// fake db api
const Db = {
  find: id =>
    Task((reject, resolve) => 
      resolve((id in data) ? Right(data[id]) : Left('not found')))
}

OK, so there's one little twist. Our Db.find function returns a Task of an Either (Left or Right). This is mostly for demonstration purposes, but also could be argued as a good practice. Ie, we might not consider user-not-found scenario an error, thus we don't want to reject the task – instead, we gracefully handle it later by resolving a Left of 'not found'. We might use reject in the event of a different error, such as a failure to connect to the database or something.


Making goals

The goal of our program is to take a given user id, and look up that user's bff.

We're ambitious, but naïve, so we first try something like this

const main = id =>
  Db.find(1) // Task(Right(User))
    .map(either => // Right(User)
      either.map(user => // User
        Db.find(user.bff))) // Right(Task(Right(user)))

Yeck! a Task(Right(Task(Right(User)))) ... this got out of hand very quickly. It will be a total nightmare working with that result...


Natural transformation

Here comes our first natural transformation eitherToTask:

const eitherToTask = e =>
  e.fold(Task.rejected, Task.of)

// eitherToTask(Left(x)) == Task.rejected(x)
// eitherToTask(Right(x)) == Task.of(x)

Let's watch what happens when we chain this transformation on to our Db.find result

const main = id =>
  Db.find(id) // Task(Right(User))
    .chain(eitherToTask) // ???
    ...

So what is ???? Well Task#chain expects your function to return a Task and then it squishes the current Task, and the newly returned Task together. So in this case, we go:

// Db.find           // eitherToTask     // chain
Task(Right(User)) -> Task(Task(User)) -> Task(User)

Wow. This is already a huge improvement because it's keeping our data much flatter as we move through the computation. Let's keep going ...

const main = id =>
  Db.find(id) // Task(Right(User))
    .chain(eitherToTask) // Task(User)
    .chain(user => Db.find(user.bff)) // ???
    ...

So what is ??? in this step? We know that Db.find returns Task(Right(User) but we're chaining, so we know we'll squish at least two Tasks together. That means we go:

// Task of Db.find         // chain
Task(Task(Right(User))) -> Task(Right(User))

And look at that, we have another Task(Right(User)) which we already know how to flatten. eitherToTask!

const main = id =>
  Db.find(id) // Task(Right(User))
    .chain(eitherToTask) // Task(User)
    .chain(user => Db.find(user.bff)) // Task(Right(User))
    .chain(eitherToTask) // Task(User) !!!

Hot potatoes! Ok, so how would we work with this? Well main takes an Int and returns a Task(User), so ...

// main :: Int -> Task(User)
main(1).fork(console.error, console.log)

It's really that simple. If Db.find resolves a Right, it will be transformed to a Task.of (a resolved Task), meaning the result will go to console.log – otherwise, if Db.find resolves a Left, it will be transformed to a Task.rejected (a rejected Task), meaning the result will go to console.error


Runnable code

// Either
const Left = x => ({
  map: f => Left(x),
  fold: (f,_) => f(x)
})

const Right = x => ({
  map: f => Right(f(x)),
  fold: (_,f) => f(x),
})

// Task
const Task = fork => ({
  fork,
  chain: f =>
    Task((reject, resolve) =>
      fork(reject,
           x => f(x).fork(reject, resolve)))
})

Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))

// natural transformation
const eitherToTask = e =>
  e.fold(Task.rejected, Task.of)

// fake database
const data = {
  "1": {id: 1, name: 'bob', bff: 2},
  "2": {id: 2, name: 'alice', bff: 1}
}

// fake db api
const Db = {
  find: id =>
    Task((reject, resolve) => 
      resolve((id in data) ? Right(data[id]) : Left('not found')))
}

// your program
const main = id =>
  Db.find(id)
    .chain(eitherToTask)
    .chain(user => Db.find(user.bff))
    .chain(eitherToTask)

// bob's bff
main(1).fork(console.error, console.log)
// alice's bff
main(2).fork(console.error, console.log)
// unknown user's bff
main(3).fork(console.error, console.log)

Attribution

I owe almost this entire answer to Brian Lonsdorf (@drboolean). He has a fantastic series on Egghead called Professor Frisby Introduces Composable Functional JavaScript. Quite coincidentally, the example in your question (transforming Future and Either) is the same example used in his videos and in this code in my answer here.

The two about natural transformations are

  1. Principled type conversions with natural transformations
  2. Applying natural transformations in everyday work

Alternate implementation of Task

Task#chain has a little bit of magic going on that's not immediately apparent

task.chain(f) == task.map(f).join()

I mention this as a side note because it's not particularly important for considering the natural transformation of Either to Task above. Task#chain is enough for demonstrations, but if you really want to take it apart to see how everything is working, it might feel a bit unapproachable.

Below, I derive chain using map and join. I'll put a couple of type annotations below that should help

const Task = fork => ({
  fork,
  // map :: Task a => (a -> b) -> Task b
  map (f) {
    return Task((reject, resolve) =>
      fork(reject, x => resolve(f(x))))
  },
  // join :: Task (Task a) => () -> Task a
  join () {
    return Task((reject, resolve) =>
      fork(reject,
           task => task.fork(reject, resolve)))
  },
  // chain :: Task a => (a -> Task b) -> Task b
  chain (f) {
    return this.map(f).join()
  }
})

// these stay the same
Task.of = x => Task((reject, resolve) => resolve(x))
Task.rejected = x => Task((reject, resolve) => reject(x))

You can replace the definition of the old Task with this new one in the example above and everything will still work the same ^_^


Going Native with Promise

ES6 ships with Promises which can function very similarly to the Task we've implemented. Of course there's heaps of difference, but for the point of this demonstration, using Promise instead of Task will result in code that almost looks identical to the original example

The primary differences are:

  • Task expects your fork function parameters to be ordered as (reject, resolve) - Promise executor function parameters are ordered as (resolve, reject) (reverse order)
  • we call promise.then instead of task.chain
  • Promises automatically squish nested Promises, so you don't have to worry about manually flattening a Promise of a Promise
  • Promise.rejected and Promise.resolve cannot be called first class – the context of each needs to be bound to Promise – eg x => Promise.resolve(x) or Promise.resolve.bind(Promise) instead of Promise.resolve (same for Promise.reject)

// Either
const Left = x => ({
  map: f => Left(x),
  fold: (f,_) => f(x)
})

const Right = x => ({
  map: f => Right(f(x)),
  fold: (_,f) => f(x),
})

// natural transformation
const eitherToPromise = e =>
  e.fold(x => Promise.reject(x),
         x => Promise.resolve(x))

// fake database
const data = {
  "1": {id: 1, name: 'bob', bff: 2},
  "2": {id: 2, name: 'alice', bff: 1}
}

// fake db api
const Db = {
  find: id =>
    new Promise((resolve, reject) => 
      resolve((id in data) ? Right(data[id]) : Left('not found')))
}

// your program
const main = id =>
  Db.find(id)
    .then(eitherToPromise)
    .then(user => Db.find(user.bff))
    .then(eitherToPromise)

// bob's bff
main(1).then(console.log, console.error)
// alice's bff
main(2).then(console.log, console.error)
// unknown user's bff
main(3).then(console.log, console.error)
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    I think you should drop the `reject` from `Task` and then represent the possibiliby of failure as `Task>`. That would be a real monad transformer! – Bergi Mar 15 '17 at 01:09
  • @Bergi that's an interesting idea. I wonder if the video series used Task as-is to not overwhelm learners with implementation details. I still have some questions about how your proposition would work. Do you think you could provide an edit for clarification? – Mulan Mar 15 '17 at 02:09
  • @Bergi, specifically if failure is represented as `Task(Left(someErr))`, do we even have a case for transformations anymore? Or would we need a completely separate example program? – Mulan Mar 15 '17 at 02:23
  • Not a natural transformation, no, but you could explicitly write out the [`Either` monad transformer](https://en.wikipedia.org/wiki/Monad_transformer#The_exception_monad_transformer) and apply it to `Task`. And out comes a monad whose `chain` you can directly apply to the `find(id)` results – Bergi Mar 15 '17 at 02:44
  • So instead of `Task>` we'd have `EitherT` if you understand what I mean – Bergi Mar 15 '17 at 02:50
  • Excelent. Thanks for that @naomik. But it left me thinking, what if I want to handle the user not found scenario differently from the database error scenario? Is there any solution that would not group them together? – Marcelo Lazaroni Mar 15 '17 at 10:45
  • This answer is gold. – Alex Pánek Mar 15 '17 at 12:49