Maybe Eithers with Promises
I want take some bits to process a recent issue of JS Weekly that reposted a new piece by Eric Elliot about optional values in JavaScript. I read it. It lit up my memory.
One of the first Lambda Cast episodes I listened to was #6: Null and Friends. At this point in time I’m near the start of my FP cultivations, sometime mid- last year I think. It’s probably around the time I was finishing Kyle Simpson’s Functional-Light JavaScript. I was fairly shocked to hear the bros discuss functional languages that are designed to keep null out of your programs. I could feel myself starting to consider the FP language hype, especially the hype around eliminating uncertainty.
Because of JavaScript’s weak type system optional values represent data that will likely cause problems somewhere in your software system. Basically the language lets us get away with answering I don’t know without any compose-time feedback. Spinning up the browser and pushing play is sometimes your only recourse.
FUNCTION A: Alright here’s some data, can I have that back nice and neat please before I send it to the user.
FUNCTION B: Yeah sure…wait…ermm…I don’t think so actually…yeah I don’t know.
FUNCTION A: Ok great I’ll just show nothing to the user forever or maybe crash the system.
FUNCTION B:
Elliot is also quick to mention the case of uninitialized data. He lists the common progenitors of null
:
- User input
- Database/network records
- Uninitialized state
- Functions which could return nothing
I’ve got an example from the office, bear with me.
In our application there is a form field. This field represents a maximum limit on the number of holds for your event ticket. There are two types of input this field can receive from a user: 1) an integer or 2) nothing (ie, be left empty). The latter signifies that the user desires to unrestrict the number of holds.
Nullable fields! These are quite commonplace and a user would never think twice about the dark alchemy we’re performing behind the scenes. What the user doesn’t know is that this field maps to a database columm that expects an integer (INT
). Thus, when the user leaves the field empty and then submits the form, we need to make sure that a “no INT” is mutated into a stringified representation of zero to slot in the POST body: {limit: 0}
. What a twisting mind eff: the zero means unlimited. Leaving the field blank is not a lack, but bountiful! 1 x 0 = FUN
. Nothing is not nothing. Therefore, a sensible default.
function serializeFormData(formData) {
const limit = formData.limit || 0; // the empty input string is nullable!
const nextField = //...
const body = {
limit,
nextField,
// ...
}
return body;
}
Of course, a reverse alchemy must occur from the other direction when our React code initializes the form to begin with. We first hand our form builder an empty object – key/value pairs with field names as keys and undefined
s as values. The builder then converts these undefined
s to appropriate defaults.
function getInitialFormData(initialData) {
const limit = initialData.limit || '';
const nextField = //...
const initialFormData = {
limit,
nextField,
// ...
}
return initialFormData;
}
Looks familiar.
Thusly, the JavaScripter putzes around with a notion of nothingness by phase-shifting duck typed values.
null
<-> ''
<-> 0
(There’s probably cooler mathematical notation for this.)
Unforch we have to use null
and friends for lack of a better optional option. Like, for lack of formal invariants in-language. It’s impossible to truly prevent these meaningless nothings from entering our JavaScript programs. (Meaningless like may never receive meaning, ambiguous, undecided. Totally void 0
: what a good euphemism from the grammar.) Like, you can’t serialize nothing for a value an API response formatted as JSON.
>>> json.dumps({name: }
File "", line 1
json.dumps({name: }
^
SyntaxError: invalid syntax
Or try and stringify an Object
back up again with the same:
>> JSON.stringify({name: })
SyntaxError: expected expression, got '}'
Null
and undefined
are optional in JS but they are not illegal. Like in Haskell, which wraps up the ambiguity in a fat Nothing.
Elliot does an interesting rhetorical jiu jitsu by giving us new options for optional values. In liue of eviscerating null from JS, we can work to push null
to the edge of our programs with a handful of innovative approaches. In a sense we can ignore nullables and declutter areas of code which can just focus on data pipelining and other UI biz logic. Techniques include: constructing state machines – highly determined object interfaces – that error without values set to a wanted data type; ie something. We can also take advantage of that new new: Optional Chaining. And then there’s borrowing from FP. The last I love.
I’ve already been thinking about Maybes a lot recently. My last post was about using “maybe” in function names to negotiate the unrealistic binary of if/else with better signaling to other developers; like, a function may or may not complete it’s designated purpose if an async call fails or an argument is unacceptable. The real word is far too fuzzy. In contrast to the imperatives of JS, FP languages substitute nullable data with algebraic structures that encapsulate possibilities of existence or nothingness. For example, the actual Maybe data type represents either a Just (truth, success, completion) or Nothing (false, error, incompatibility). Data that’s wrapped in a Maybe and operated on won’t leak a nullable into our program, like the commonly observed undefined
. Obviously implementations vary across libaries. Here’s a simple example from the Practica library which demonstrates the way that using Maybe can simplify code:
import { Maybe } from 'pratica';
const data = await fetchAllPeople(...);
Maybe(data)
.map(people => people.filter(person => person.cool))
.map(people => people.map(getNames))
.map(names => name.toUpperCase())
.cata({
Just: transformedData => render(transformedData),
Nothing: () => console.log('Womp, no data returned from API.')
})
(Btw, “cata” stands for catamorphism and means to decompose the Maybe container into simple values. Honestly, I’m not good enough in the category theory yet to confidently distill it for you completely – pun intended – but that’s the gist.)
A more basic JS solution might look like:
const data = await fetchAllPeople(...);
if (data) {
const coolPeople = people => people.filter(person => person.kind);
if (coolPeople.length) {
const formattedKindPeople = people => people.map(formatPersonForDisplay);
render(formattedKindPeople)
} else {
console.log('Womp, only unkind people.')
}
} else {
console.log('Womp, no data returned from API.')
}
The combination of FP-style data pipelining – allowed by Maybe’s monadic interface, I think? – and control flow encapsulated in the data type itself, we get a semantically rich and easy-to-read solution without nullables and exhausting boilerplate; ie, param existence checks.
Where Elliot really surprised me was drawing a line between FP’s similar-to-Maybe data type Either and JS’s Promise. Tucking null
away with Promises is super neat. Let’s see how that plays out in a sec.
While maybes represent one or no value, Just or Nothing, Either implementations are slightly different in that they represent one or the other, but not both. If you’re familiar with bitwise XOR, it’s the same algorithm, except that in place of a Nothing or performing a noop, Eithers provide a secondary branch for an error case. Let’s see it in action.
Take Elliot’s example of a small abstraction that hides null
checking away in a kind of promisified ternary (which I’ve slightly modified):
const exists = (x) => x !== null;
const ifExists = (value) =>
exists(value)
? Promise.resolve(value)
: Promise.reject(`Invalid prop: ${value}`);
ifExists(prop.name).then(renderName).catch(log);
Here basic null checking and *primitive" if/else binaries are replaced with a more expressive, semantically rich statement for the logical disjunction: proceed this way if success, or that way.
Now, logging an error doesn’t get us very far from param checking and early returns. A slightly more interesting example might be something like:
const inputExists = x => x !== '';
const ifInputExists = value => inputExists(value) ?
Promise.resolve(value) :
Promise.reject(`Input is blank`);
onInput((prevValue, nextValue) =>
ifInputExists(nextValue)
.then(validate)
.catch(trackClearInput(prevValue))
It’s hard to see the real power of this for a simple resolve/reject example. It just feels like a fancy if/else, right? But if we extrapolate from this base interesting things start to happen. Here’s a slightly modified version of an example from Practica’s docs with an imaginary Either that uses Promises under the hood and implements a chain
behavior:
const isPerson = p => p.name && p.age
? Promise.resolve(p)
: Promise.reject('Not a person')
const isOlderThan2 = p => p.age > 2
? Promise.resolve(p)
: Promise.reject('Not older than 2')
const isJason = p => p.name === 'jason'
? Promise.resolve(p)
: Promise.reject('Not jason')
const person = awaitfetchPerson(...);
Either(person)
.chain(isPerson)
.chain(isOlderThan2)
.chain(isJason)
.then(p => console.log('this person satisfies all the checks'))
.catch(msg => console.log(msg)); // if any checks reject, then this function will be called. If isPerson rejects, then isOlderThan2 and isJason functions won't even execute, and the err msg would be 'Not a person'
Suffice to say I’m quite tickled by the re-purposing of Promises as Eithers. You can start to imagine how one might construct custom-fit control flow chains and layer cakes using thenable types to play nicely with other function composition and pipelining. I’m not always in love with (what feels like) sacrificed readability with chaining over stacked lines of assigned returns or async/await. But seeing it an action using Practica I’m starting to believe more and more in its viability, even in a codebase touched by less experienced or new developers.
Gripes with FP readability aside, it’s eye-opening to look at available JS language features and see them in a different light. Also, aside from the clever use of Promises, just getting the null check into an abstraction exists(...)
already has us using an FP mindset to build strong declarative (function-first) foundations.
Wednesday December 11, 2019