05 - A collection of Either examples compared to imperative code#
Here’s the video lesson.
Remember that when we use fromNullable()
we are actually returning
an Either
type: either a Left
or a Right
container which
enables us to chain invocations in a composable, functional stylish
approach not requiring assignments and the use of imperative style.
openSite()#
An example of a function that would return the login page for non logged in (unauthenticated) users and a welcome page for logged in (authenticated) users.
Unit Tests#
// import { openSite } from './openSite-v1.js';
import { openSite } from './openSite-v2.js';
describe('openSite()', () => {
it('should display login page when user does not exist', () => {
expect(openSite(undefined)).toEqual('Sign In!');
});
it('should display home page when user exists', () => {
expect(
openSite({ id: 1, name: 'Ahsoka Tano' })
).toEqual('Welcome to our site!');
});
});
Helper Functions#
A few helper functions used in the openSite()
function.
//
// NOTE: Don't mind these silly functions. It is just to show
// the main logic as in the video. It is not the point to actually
// make real DOM-rendering stuff here 😄.
//
function showLogin() {
return 'Sign In!';
}
function renderPage() {
return 'Welcome to our site!';
}
export {
showLogin,
renderPage,
};
Imperative Style#
We are using a conditional operator here (instead of the if else as in the video because the condition in this example is short and concise enough that using the lengthier if else doesn’t gain us any readability in this case.
Tip
We colloquially say ternary operator, but the name used in the spec is indeed conditional operator. See the MDN docs on the conditional operator and the spec on the conditional operator.
import { showLogin, renderPage } from './openSite-helpers.js';
/**
* Given the user, renders the welcome or the login page.
*
* ASSUME: If we have an user, it is a logged in user.
*
* @param {undefiend|object}
* @return {string}
*/
function openSite(user) {
return user ? renderPage() : showLogin();
}
export { openSite };
Functional, Composable Style#
import { fromNullable } from '../lib/index.js';
import { showLogin, renderPage } from './openSite-helpers.js';
/**
* Given the user, renders the welcome or the login page.
*
* ASSUME: If we have an user, it is a logged in user.
*
* @param {undefiend|object}
* @return {string}
*/
function openSite(user) {
return fromNullable(user).fold(showLogin, renderPage);
}
export { openSite };
getPrefs()#
In this example, the getPrefs()
function returns default
preferences for non-premium (free tier) users, and the user
preferences for premium users.
Unit Tests#
import { getPrefs } from './getPrefs.js';
import defaultPrefs from './getPrefs-default-prefs.json';
describe('getPrefs()', () => {
describe('when user is on free tier', () => {
it('should get default preferences', () => {
const user = {
id: 2,
name: 'Obi-wan Kenobi',
premium: false,
};
expect(getPrefs(user)).toEqual(defaultPrefs);
});
});
describe('when user is premium', () => {
it('should get user preferences', () => {
const user = {
id: 1,
name: 'Aayla Secura',
premium: true,
prefs: {
theme: 'light',
layout: 'column',
}
};
expect(getPrefs(user)).toEqual(user.prefs);
});
});
});
TypeDefs#
We also have some JSDoc types so we get nice intellisesnse and documentation while coding! This is no TypeScript. This is Vanilla JavaScript, but TSServer does an incredible job with the types from the JSDoc comments!
/**
* @typedef {object} Prefs
* @property {string} theme
* @property {string} layout
*/
/**
* @typedef {object} User
* @property {number} id
* @property {string} name
* @property {boolean} premium
* @property {Prefs} prefs
*/

Imperative Style#
/// <reference path="./getPrefs-typedefs.js" />
import defaultPrefs from './getPrefs-default-prefs.json';
/**
* Gets user preferences.
*
* For premium users, return their personal preferences; for free-tier
* users, return default preferences.
*
* @param {User} user
* @return {Prefs}
*/
function getPrefs(user) {
const { premium, prefs } = user;
return premium ? prefs : defaultPrefs;
}
export { getPrefs };
Functional, Composable Style#
In this case, we are not using the Either
type (Left
and
Right
) to indicate error and success conditions, but to capture
non-premium and premium user contexts, and chain logic in a composable
way based on that.
/// <reference path="./getPrefs-typedefs.js" />
import { Left, Right } from '../lib/index.js';
import defaultPrefs from './getPrefs-default-prefs.json';
/**
* Gets user preferences.
*
* For premium users, return their personal preferences; for
* free-tier users, return default preferences.
*
* @param {User} user
* @return {Prefs}
*/
function getPrefs(user) {
const { premium, prefs } = user;
return (premium ? Right(user) : Left('not premium'))
.fold(() => defaultPrefs, () => prefs);
}
export { getPrefs };
You will notice that I did not include the loadPrefs()
as in the
video. A few reasons for this decision are:
It didn’t seem important for the example.
Calling
loadPrefs()
from insidegetPrefs()
seems wrong.getPrefs()
sounds like a getter, or a reader accessor (in Ruby parlance) that should be used for that single and only purpose.
getStreetName()#
This example is about getting a street name from an address. But what if we don’t have an address value/object? And if we have that, what if street is missing from that address object? Those potential problems are the topic of this next example.
Note
In the video, perhaps for conciseness, the function is named
streetAddress
. That does not sound like a function, but as a
variable. Functions and methods should be like verbs “normal”
variables, classes and other similar things should be like
nouns. So, streetName
is a variable which presumably contains
the string name of a street, while getStreetName
is a function
that would, given some input, retrieve/parse/extract the name of
the street. In this example, I have decided to follow this
rationale.
Unit Tests#
// import { getStreetName } from './getStreetName-v1.js';
// import { getStreetName } from './getStreetName-v2.js';
// import { getStreetName } from './getStreetName-v3.js';
import { getStreetName } from './getStreetName-v4.js';
describe('getStreetName()', () => {
describe('when address is empty', () => {
it('should return "no street"', () => {
const address = undefined;
const user = { address };
expect(getStreetName(user)).toEqual('no street');
});
});
describe('when address object exists but street is empty', () => {
it('should return "no street"', () => {
const user1 = {
name: 'User 1',
address: {
id: 1,
/* Note no 'street' property */
},
};
const user2 = {
name: 'User 2',
address: {
street: undefined,
}
};
[user1, user2].forEach(user => {
expect(getStreetName(user)).toEqual('no street');
});
});
});
describe('when address and street exist', () => {
it('should return the street name', () => {
const user1 = {
name: 'User 1',
address: {
street: {
name: 'Linux Torvalds Linux Avenue',
},
},
};
const user2 = {
name: 'User 1',
address: {
street: {
name: 'Braam Moolenaar Road of Vim',
},
},
};
[user1, user2].forEach(user => {
expect(getStreetName(user)).toEqual(user.address.street.name);
});
});
});
});
Imperative Style#
This is the example precisely as in the video.
/// <reference path="./getStreetName-typedefs.js" />
/**
* Gets the street name, if possible.
*
* @param {User} user
* @return {string = 'no street'} The stret name or
*/
function getStreetName(user) {
const address = user.address;
if (address) {
const street = address.street;
if (street) {
return street.name;
}
}
return 'no street';
}
export { getStreetName };
To be fair, we can try to improve its imperative style a little. Let
us of destructuring to avoid things like const street =
address.street
, that is, repeat ‘street’ twice, and make use of
early returns to remove some of the nesting inside the if
blocks.
/// <reference path="./getStreetName-typedefs.js" />
/**
* Gets the street name, if possible.
*
* @param {User} user
* @return {string = 'no street'} The stret name or
*/
function getStreetName(user) {
const noStreet = 'no street';
const { address } = user;
if (!address) return noStreet;
const { street } = address;
if (!street) return noStreet;
return street.name;
}
export { getStreetName };
The above is still repetitive, though. With a library like Ramda we could do something as simple as this:
function getStreetName(user) {
return pathOr('no street', ['address', 'street', 'name', user]);
}
As you see, no nesting and no complex logic. We can also use ECMAScript Optional Chaining (?.) to help write concise and clean code:
function getStreetName(user) {
return user?.address?.street?.name ?? 'no street';
}
Note that we also used the Nullish coalescing operator (??) avoid
values other than undefined
and null
to be treated as falsy
values.
Functional, Composable Style#
With this example we finally bring our Either
type into the
game. Recall that fromNullable
returns either a Left
or a
Right
container.
/// <reference path="./getStreetName-typedefs.js" />
import {
fromNullable,
id,
} from '../lib/index.js';
/**
* Gets the street name, if possible.
*
* @param {User} user
* @return {string = 'no street'} The stret name or
*/
function getStreetName(user) {
return fromNullable(user.address)
.chain(address => fromNullable(address.street))
.map(street => street.name)
.fold(_ => 'no street', id);
}
export { getStreetName };
Note that we chain
on the address
because we don’t want a
container inside a container. Then, we map
over street
and
retrieve its name
property. That is feed into fold
. If
name
is falsy (like undefined
or an empty string), fold
runs the left function which returns ‘no street’; otherwise it applies
the identity function (id
) to simply return the name of the street.
We could change the syntax a bit to reduce the number of things like:
address => address.street
street => street.name
… so it becomes more like this:
({ street }) => street
({ name }) => name
Here it is:
function getStreetName({ address }) {
return fromNullable(address)
.chain(({ street }) => fromNullable(street))
.map(({ name }) => name)
.fold(_ => 'no street', id);
}
I don’t think it is necessarily better, but it is nice as well.
concatUniq()#
Unit Tests#
// import { concatUniq } from './concatUniq-v1.js';
import { concatUniq } from './concatUniq-v2.js';
describe('concatUniq()', () => {
describe('when the array already contains the value', () => {
it('should not append the value to the array', () => {
expect(concatUniq(0, [0, 1, 2])).toEqual([0, 1, 2]);
expect(concatUniq('z', ['x', 'y', 'z'])).toEqual(['x', 'y', 'z']);
});
});
describe('when array does not contain the value', () => {
it('should append the value to the array', () => {
expect(concatUniq(0, [1, 2])).toEqual([1, 2, 0]);
expect(concatUniq('y', ['x', 'z'])).toEqual(['x', 'z', 'y']);
});
});
});
Imperative Style#
The original code in the video for the imperative style is this:
function concatUniq(x, ys = []) {
const found = ys.filter(y => y === x)[0];
return found ? ys : ys.concat(x);
}
But many things can be considered falsy (zero, empty strings, among
other things), which means, if x
is, for instance, 0, and ys
contains zero, it is “found” but the way the condition is being tested
is falsy (because zero is falsy) and we’ll append another zero to an
array which already contains zero. It will not “concat uniquely”. It
will duplicate values in some cases.
concatUniq(0, [1, 0, 2]);
// → [1, 0, 2, 0]
I therefore changed to explicitly only consider something to be found
if the result is undefined
. Array.prorotype.filter
returns an
empty array if no element in the array being filtered satisfies the
predicate. But note the code uses subscript notation and attempts to
retrieve the element at index 0, which produces undefined
if the
filter
finds nothing and returns an empty array.
function concatUniq(x, ys = []) {
const found = ys.filter(y => y === x)[0];
return found !== undefined ? ys : ys.concat(x);
}
Perhaps this would convey intent a little better‽
function concatUniq(x, ys = []) {
const found = ys.filter(y => y === x).length > 0;
return found ? ys : ys.concat(x);
}
Here’s the full example with JSDocs:
/**
* Concatenates the `x` value to `ys` array only if the array does not
* already contain that `x` value.
*
* ASSUME: `x` and the elements of `ys` are primitive values that can be
* compared with the triple equals operator `===`.
*
* @sig a -> [a] -> [a]
*
* @param {any} x A value to be appended to `ys`.
* @param {any[]} = [ys = []] ys A list of values.
* @return {Array}
*
* @example
* // Does not append 0 to the array because it already contains
* // 0. That is, it does NOT return `[0, 1, 2, 0]`.
* concatUniq(0, [0, 1, 2]);
* // → [0, 1, 2]
*
* concatUniq(0, [1, 2]);
* // → [1, 2, 0]
*/
function concatUniq(x, ys = []) {
const found = ys.filter(y => y === x).length > 0
return found ? ys : ys.concat(x);
}
export { concatUniq };
Functional, Composable Style#
import { fromNullable } from '../lib/index.js';
/**
* Concatenates the `x` value to `ys` array only if the array does not
* already contain that `x` value.
*
* ASSUME: `x` and the elements of `ys` are primitive values that can be
* compared with the triple equals operator `===`.
*
* @sig a -> [a] -> [a]
*
* @param {any} x A value to be appended to `ys`.
* @param {any[]} = [ys = []] ys A list of values.
* @return {Array}
*
* @example
* // Does not append 0 to the array because it already contains
* // 0. That is, it does NOT return `[0, 1, 2, 0]`.
* concatUniq(0, [0, 1, 2]);
* // → [0, 1, 2]
*
* concatUniq(0, [1, 2]);
* // → [1, 2, 0]
*/
function concatUniq(x, ys = []) {
return fromNullable(ys.filter(y => y === x)[0])
.fold(_ => ys.concat(x), _ => ys);
}
export { concatUniq };
Note that here we went back to using subscript notation to retrieve
the first element of the filtered array (returned from filter
). It
works fine because formNullable
internally uses isNil
, which
correctly only considers undefined
and null
to be falsy (nil)
values. In fact, we cannot use the .length > 0
– which produces a
boolean – in this case precisely because of isNil
.
We also used _
in fold
left and right functions to ignore the
parameters (and avoid linters and TSServers complaints about unused
variables. We only use variables from the enclosing scope inside the
fold()
.
Final Notes#
Interesting comment from Brian for a question in this video:
“I think the best practice is to return Either’s from low level partial functions that might return null. If they do that, then when you’re at an application level, you’ve ensured existence at the edges so you don’t end up with a bunch of unnecessary (paranoia-driven-development) checks in your main logic.”
—Brian Lonsdorf