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.
Rules first
First we have the Natural Transformation Law
F
of a
, mapped with function f
, yields F
of b
, then naturally transformed, yields some functor G
of b
.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
.
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 chain
ing, so we know we'll squish at least two Task
s 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
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:
fork
function parameters to be ordered as (reject, resolve)
- Promise executor function parameters are ordered as (resolve, reject)
(reverse order)promise.then
instead of task.chain
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With