Composability: from Callbacks to Categories in ES6

  • Callbacks
  • Composable Callback class
  • Composable Callback class and Promise class are Monads
  • Categories
  • Func Category
  • Kleisli Category

Callbacks

db.getSomething(callback)
function next2(x, callback) {
callback(x + 1, x + 2)
}
next2(10, (eleven, twelve) => …)
function sqrt(x, callback) { 
if(x < 0)
callback(Error('Sqrt of negative value', null))
else
callback(null, Math.sqrt(x))
}
function myLongOperation(userId, callback) {
db.getUser(userId, (error, user) => {
if(!!error)
return callback(error, null)
else
api.generateMessage(user, (error, message) => {
if(!!error)
return callback(error, null)
else
client.sendMessage(message, callback)
})
})
}
getUser(userId).then(generateMessage).then(sendMessage)
const plus1 = x => x + 1
userId → ( … ⋙ … ⋙ … )

Composable Callback

  • constructor takes an async function (f which will produce either an error or a value x)
  • run instance function: receives a callback function and feed it to the f
  • map instance function analogous to Array.map, transforms the x (the result of f)
  • bind instance function is similar to Promise.then, it is used for chaining Callback instances
  • then instance function corresponds to Promise.then; it’s a combination of map and bind functions.
  • bindTo instance function is a utility for chaining Callback instances to normal async functions
  • pure (alias resolve) static function is similar to Promise.resolve, it creates an instance of Callback.
  • from static function casts an async function to an instance of Callback.
Callback.pure(64).run((error, result) => console.log(result))
Callback.pure(64)
.bindTo(sqrt)
.run((error, result) => console.log(error || result))
Callback.pure(36)
.then(x => new Callback(cb => sqrt(x, cb)))
.run((error, result) => console.log(error || result))
// userId → (getUser ⋙ genMessage ⋙ sendMessage)const myLongOperation = (userId, callback) => 
Callback.pure(userId)
.bindTo(getUser).bindTo(genMesssage).bindTo(sendMessage)
.run(callback)
  • .bindTo(getUser).bindTo(genMesssage).bindTo(sendMessage) is denoted by (getUser ⋙ genMessage ⋙ sendMessage)
  • But Callback.pure(userId) seems unnecessary. (userId → (…) is the denotation of the whole myLongOperation function.) We will back to this point later.
const messageUser = (userName, callback) =>
Callback.pure(userName).bindTo(getUserId).bindTo(myLongOperation)
.run(callback)
const proc = Callback.pure(5)
.then(x => new Callback(cb => {
console.log(`binding ${x} to x + 1`)
setTimeout(() => cb(null, x + 1), 100)
}))
const prom = new Promise(resolve => {
console.log('Promise executes immediately')
resolve()
})
// userId → (getUser ⋙ genMessage ⋙ sendMessage)const myLongOperation = userId => 
Callback.pure(userId)
.bindTo(getUser).bindTo(genMesssage).bindTo(sendMessage)
// userName → (getUserId ⋙ myLongOperation)const messageUser = userName =>
Callback.pure(userName).bindTo(getUserId).then(myLongOperation)
messageUser(userName).run((error, result) => ...)

Callback and Promise are Monads

const proc = Callback.pure(10)
proc.bind(x => new Callback(…))
  • proc is a Callback of x
  • proc.bind is a (higher order) function that takes a function from x to Callback of y and produces a Callback of y.
Callback.pure(10) :: Callback 10
Callback.pure(10)
.bind(x => new Callback(cb => cb(null, x + 1)))
Promise.resolve(10)
.then(x => new Promise(resolve => resolve(x + 1)))
Monad 10           ::  Promise.resolve(10)
≫= :: .then(…)
x → Monad (x + 1) :: x => new Promise(resolve => resolve(x + 1))
Monad 10           ::  Callback.resolve(10) // = Callback.pure(10)
≫= :: .then(…) // = Callback.bind(…)
x → Monad (x + 1) :: x => new Callback(cb => cb(x + 1))

What is a Monad?

  • The class must have a way of encapsulating a value (using a static pure() or resolve() function)
  • It must provide a way to bind itself with a function that returns another instance of it (using bind() or then())
Promise.resolve = x => new Promise(res => res(x))
Callback.pure = x => new Callback(cb => cb(null, x))
Array.of = x => [x] 
Reader.pure = x => new Reader(env => x)
Identity.pure = x => new Identity(x)

Categories

const myLongOperation = userId => 
Callback.pure(userId)
.bindTo(getUser).bindTo(genMesssage).bindTo(sendMessage)
myLongOperation(123456).run((error, result) => ...)
  • get a user
  • generate a message for that user
  • send that message (and asynchronously return SendMessageResult)

Good old functions

const plus1  = x => x + 1
const times2 = x => x * 2
const compose = (f, g) => x => f(g(x))
compose(plus1, times2)(10) == 21
const pipe = (f, g) => x => g(f(x))pipe(plus1, times2)(10) // == 22
pipe(plus1, times2)(10) != pipe(times2, plus1)(10)
const id = x => x
pipe(times2, id)(10) // == 20
pipe(id, times2)(10) // == 20
  • They’re composable (pipe-able)
  • They have a special id instance for which the composition is commutative

Func Category

Func.id.pipe(new Func(x => x * 2)).run(10) // = 20
new Func(x => x * 2).pipe(Func.id).run(10) // = 20
new Func(x => x * 2).run(5) == (x => x * 2)(5)
new Func(x => x * 2)
.pipe(new Func(x => x + 1))
.pipe(new Func(x => Math.sqrt(x)))
.run(12) // == 5
Callback.pure(12)
.then(x => Promise.resolve(x * 2))
.then(x => Promise.resolve(x + 1))
.then(x => Promise.resolve(Math.sqrt(x)))
.run((error, result) => console.log(result) /* result == 5 */)
.bind(x => new Callback(cb => cb(null, x + 1)))
.pipe(new Func(x => x + 1))

Functions that return a Monad form a Category

const times2Plus1 = new Kleisli(x => Promise.resolve(x * 2))
.pipeTo(x => Promise.resolve(x + 1))
times2Plus1.run(10)
.then(x => console.log(x)) // == 21
.catch(error => console.error(error))
const times2Plus1 = new Kleisli(x => Promise.resolve(x * 2))
.pipe(new Kleisli(x => Promise.resolve(x + 1)))
// myLongOperation :: Category (userId ↣ Promise SendMessageResult)const myLongOperation = new Kleisli(getUser)
.pipeTo(genMesssage)
.pipeTo(sendMessage)
myLongOperation.run(123456).then(sendMessageResult => …)
getUser     = userId => new Promise(resolve => … resolve(user))
genMessage = user => new Promise(resolve => … resolve(msg))
sendMessage = msg => new Promise(resolve => … resolve(SendResult))

In conclusion

Callback.pure(10).then(x => new Callback(cb => cb(null, x + 1)))Callback.resolve(10).then(x => new Promise(res => res(x + 1))
Func(x => x + 1).pipe(new Func(x => x * 3)).run(10)

--

--

--

Me: http://homam.me/ | My code: https://github.com/homam/

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Homam

Homam

Me: http://homam.me/ | My code: https://github.com/homam/

More from Medium

.map(), .filter() and .reduce() in JavaScript

Common JS Async / Await Patterns That You Shouldn’t Use

Abstractions in Action: ORM’s and Javascript

SPAs: from Server to Client and back