natural transformations
Nesting of data containers can get messy, but there's a well-known technique for keeping them flat – eitherToPromise
below is considered a natural transformation – it converts an Either to a Promise which allows it to be flattened in the .then
chain, but also simultaneously prevents your value/error wires from getting crossed
Note: You probably want to use makeAICCall
to return an Either
(Left
, Right
) instead of Maybe
because you'll be able to return an error message (instead of Nothing
, which is less informative)
import { Left, Right, eitherToPromise } from './Either'
const makeAICCall = (...) =>
someCondition
? Left (Error ('error happened'))
: Right (someResult)
const getToken = p =>
makeAICCall (p.aiccsic, p.aiccurl) // => Promise<Either<x>>
.then (eitherToPromise) // => Promise<Promise<x>>
// => auto-flattened to Promise<x>
.then (syncUserWithCore) // => Promise<x>
.then (managejwt.maketoken) // => Promise<x>
Supply your favourite implementation of Either
// Either.js
export const Left = x =>
({
fold: (f,_) => f (x),
// map: f => Left (x),
// chain: ...,
// ...
})
export const Right = x =>
({
fold: (_,f) => f (x),
// map: f => Right (f (x)),
// chain: ...,
// ...
})
export const eitherToPromise = m =>
m.fold (x => Promise.reject (x), x => Promise.resolve (x))
runnable demo
const someAsyncCall = x =>
new Promise (r => setTimeout (r, 1000, x))
const authenticate = ({user, password}) =>
password !== 'password1'
? Left (Error ('invalid password'))
: Right ({user, id: 123})
const someSyncCall = token =>
Object.assign (token, { now: Date.now () })
const getToken = x =>
someAsyncCall (x)
.then (authenticate)
.then (eitherToPromise)
.then (someSyncCall)
// minimal dependencies
const Left = x =>
({ fold: (f,_) => f (x) })
const Right = x =>
({ fold: (_,f) => f (x) })
const eitherToPromise = m =>
m.fold (x => Promise.reject (x), x => Promise.resolve (x))
// test it
getToken ({user: 'alice', password: 'password1'})
.then (console.log, console.error)
// 1 second later ...
// { user: 'alice', id: 123, now: 1509034652179 }
getToken ({user: 'bob', password: 'password2'})
.then (console.log, console.error)
// 1 second later ...
// Error: invalid password ...
hey, lookit that
Our solution above results in a sequence of .then
calls – an answer to your previous question demonstrates how such a program can be expressed in a different way
nullables
You should try your best to write functions that have a well-defined domain and codomain – you should be able to say, for example,
My function takes a string (domain) and returns a number (codomain) – anonymous wiseman
And avoid writing functions that have descriptions like,
It can take a number or a string and it returns an array, but could also return undefined. Oh and sometimes it can throw an error. But that's it, I'm pretty sure. – anonymous ignoramus
But of course we'll be dealing with null
and undefined
sometimes. How can we deal with it in a "functional way" – that's what you're wondering, right?
If you find yourself in an encounter with a function's nullable codomain (ie, can return a nullable), we can create a little helper to coerce it into a type we want. We'll demonstrate again with Either, just to tie it into the original code later
const Left = x =>
({ fold: (f,_) => f (x) })
const Right = x =>
({ fold: (_,f) => f (x) })
const eitherFromNullable = (x, otherwise = x) =>
x === null ||
x === undefined
? Left (otherwise)
: Right (x)
// !! nullable codomain !!
const find = (f, xs) =>
xs.find (x => f (x))
// example data
const data =
[1,2,3,4,5]
// perform safe lookups by wrapping unsafe find in eitherFromNullable
eitherFromNullable (find (x => x > 3, data))
.fold (console.error, console.log)
// <console.log> 4
eitherFromNullable (find (x => x > 5, data))
.fold (console.error, console.log)
// <console.error> undefined
eitherFromNullable (find (x => x > 5, data), Error (`couldn't find a big number !`))
.fold (console.error, console.log)
// <console.error> Error: couldn't find a big number !
nullables and natural transformations
Remember, we do our best to avoid nullables, but sometimes we can't help it. To show how this might tie in with the original code, let's pretend that instead of returning an Either, makeAICCall
will instead return some x or some null
We just screen it with eitherFromNullable
– new code in bold
const getToken = p =>
makeAICCall (p.aiccsic, p.aiccurl) // => Promise<x?> could be null !!
.then (x => // => Promise<Either<x>>
eitherFromNullable (x, Error ('bad aic call')))
.then (eitherToPromise) // => Promise<Promise<x>>
// => auto-flattened to Promise<x>
.then (syncUserWithCore) // => Promise<x>
.then (managejwt.maketoken) // => Promise<x>
stop hating lambdas
You probably wanna ditch that lambda, right ? Ok fine, just don't make it into a fetish.
const eitherFromNullable = otherwise => x =>
x == null ? Left (otherwise) : Right (x)
// ooooh, yeah, you like that
makeAICCall (p.aiccsic, p.aiccurl)
.then (eitherFromNullable (Error ('bad aic call')))
.then (eitherToPromise)
.then ...
don't get stuck
Nullable doesn't mean anything – you decide what it means in the context of your program.
const eitherFromNullable = (x, otherwise = x) =>
// we consider null and undefined nullable,
// everything else is non-null
x === null ||
x === undefined
? Left (otherwise)
: Right (x)
You could decide that false
and 0
and empty string ''
are also "nullable" – Or, you could just as easily decide to have very specific adapters eitherFromNull
, eitherFromUndefined
, eitherFromBoolean
, etc – it's your program; it's up to you!
I feel like I'm starting to repeat myself ^_^'
make it routine
So you're saying you have lots of areas in your program where nulls are just unavoidable; maybe it's some dependency that you cannot get rid of. We'll imagine your code base with the following
// no one wants to do this for every endpoint!
const getUser = id =>
new Promise ((resolve, reject) =>
request ({url: '/users', id}, (err, res) =>
err
? reject (err)
: res.status === 403
? reject (Error ('unauthorized'))
res.body == null ?
? reject (Error ('not found'))
: resolve (User (JSON.parse (res.body)))))
- It's using
request
which has an older Node-style callback interface that we're tired of wrapping in a promise
- The API endpoints will respond with a
403
status if the requestor is unauthorized to view the requested resource
- The API endpoints we talk to sometimes respond with
null
with status 200 (instead of 404) for missing resources; for example /users/999
, where 999
is an unknown user id, will not trigger an error, but will fetch an empty response body
- The API will respond with a valid JSON document for all other requests
We wish we could use something other than request
, but our supervisor says No. We wish the API endpoints had different behavior, but that's out of our control. Still, it's within our power to write a good program
// functional programming is about functions
const safeRequest = (type, ...args) =>
new Promise ((resolve, reject) =>
request[type] (args, (err, res) =>
err
? reject (err)
: res.status === 403
? reject (Error ('unauthorized'))
res.body == null ?
? reject (Error ('not found'))
: resolve (JSON.parse (res.body))))
const getUser = id =>
safeRequest ('get', {url: '/users', id})
const createUser = fields =>
safeRequest ('post', {url: '/users', fields})
const updateUser = (id, fields) =>
safeRequest ('put', {url: '/users', id, fields})
Can it be improved more? Sure, but even if that's as far as you went, there's nothing wrong with that; all of the necessary checks happen for each endpoint because they were defined using safeRequest
Ok, so you wanna take it further? No problem. It's your program, do whatever you want!
const promisify = f => (...args) =>
new Promise ((resolve, reject) =>
f (...args, (err, x) =>
err ? reject (err) : resolve (x)))
const promiseFromResponseStatus = res =>
res.status === 403 // or handle other status codes here too !
? Promise.reject (Error ('unauthorized'))
: Promise.resolve (res)
const promiseFromNullableResponse = res =>
res.body == null // or res.body == '', etc
? Promise.reject (Error ('not found'))
: Promise.resolve (res.body)
const safeRequest = (type, ...args) =>
promisify (request [type]) (...args)
.then (promiseFromResponseStatus)
.then (promiseFromNullableResponse)
.then (JSON.parse)
const getUser = id =>
safeRequest ('get', {url: '/users', id})
const createUser ...
....