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-v1.js';
import { getPrefs } from './getPrefs-v2.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
 */
../_images/vim-getPrefs-jsdoc-intellisense.png

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 inside getPrefs() 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