paint-brush
Programmer's Version of Schrodinger’s Cat: Options for JavaScriptby@sambernheim

Programmer's Version of Schrodinger’s Cat: Options for JavaScript

by Sam BernheimAugust 28th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Bringing Options to JavaScript and TypeScript.

Coin Mentioned

Mention Thumbnail
featured image - Programmer's Version of Schrodinger’s Cat: Options for JavaScript
Sam Bernheim HackerNoon profile picture


Options are a powerful functional programming construct. They are commonly one of the first constructs mentioned when learning functional programming.


One nice way to think of them is as the programmer’s version of Schrodinger’s cat. There’s a box, and inside the box, there may be a value. Or maybe there isn’t. In either case, it’s impossible to know until the box is opened.


To bring the Option object to JavaScript and TypeScript, I wrote and published a fully typed, zero-dependency implementation of the functional programming Option object to NPM called excoptional.


The README also reviews Options and their capabilities and contains more information on various methods and capabilities. Take a look if you’re interested in learning more after reading this piece.


More formally, Options are an abstraction that works well with functions that may or may not return a value. Options often obviate the need for null and undefined checks and provide mechanisms to safely and declaratively transform the value they may (or may not) contain.

Using Options

Options take care of the repetitive null and undefined checks that might otherwise be needed when working with values returned by functions that may or may not have a value.


Here’s a quick example with two versions of uppercasing function. One takes a string and returns either a string or undefined.


The other takes an Option<string> and returns an Option<string>. Note the uniformity of the return type compared to string | undefiend in the first example.


const { Some, None } = require('excoptional');

// Without Options
const uppercaseStr = (value) => {

    if (value !== undefined) {
        return value.toUpperCase();
    }

    return undefined;
}

const valOne = uppercaseStr("hello world"); // => string
const valTwo = uppercaseStr(undefined); // => undefined

// With Options
const valThree = Some("hello world") // An option containing "hello world"
    .map(str => str.toUpperCase()); // => Option<string>

const valFour = None() // An option with no underlying value
    .map(str => str.toUpperCase()); // => Option<string>


In the first example, the argument is checked to make sure it’s not undefined.


TypeScript’s strict mode helps in these circumstances, but only in validating that when calling the function, a string value is always passed in (instead of undefined). The function can still return undefined and callers of uppercaseStrOne will have to check the return value is again not undefined before passing the result to the next function (or implement these undefined checks in every function).


In the second example, we call the map method on the Option passing in a function that transforms the underlying value (by uppercasing it). No undefined check is needed in the function passed to map.


This works regardless if the Option contains an underlying value or not like with valFour


The result remains an Option<string> allowing for it to be continuously chained with additional map calls that further transform the underlying value. No more undefined or null checks needed. Since the type remains an Option<string>, the context that there may not be a value here is maintained.


Options provide the ability to remove errors (like the one below) by wrapping values that may be undefined in an Option.


Uncaught TypeError: Cannot read property 'foo' of undefined at myIncrediblyImportantFunction (index.js:8)

Constructing Instances of Options

Using Options necessitates constructing instances. The example above uses excoptional which expose functions Some and None which, when invoke return Options.


Some() takes one argument and is used to create an instance of an Option with that argument as the underlying value. None() is used when there is no underlying value and so takes no arguments.


Depending on the library, creating instances may be slightly different but they all have similar semantics.

Working with Options

Working with functions that return Options instead of direct values (along with null and undefined) leads to a new set of semantics and usage patterns, but Options provide solutions for all scenarios.


Let’s add a validation function before upper casing to check the string length is sufficiently long.


From the example above, the map method worked for a transformation function that returned a value that is not an Option. What about transformation functions that themself return an Option? Here’s one.


const { Some, None } = require('excoptional');

const getIfValid = (val) => {
   if (val.length > 2) {
        return Some(val);
    } else {
        return None();
    }
};

Some("hi")
    .flatMap(getIfValid)
    .log(); // Log the result 


If the provided transformation function returns an Option, we must use flatMap. If map were to be used, the result would be a nested Option. To see this, copy the above snippet into runkit and change flatMap to map. Try also shortening the string to 2 characters or fewer.


You can also npm install the module (excoptional) locally to run examples and test out its various methods.


The TLDR here is:

  • use map when the function does not return an Option
  • use flatMap when the function does return an Option


Look out for the And Then… section for an even better solution

Getting the Value

Eventually, at some point, the underlying value is needed. The goal though is to avoid extracting it for as long as possible and instead pass around the Option instance. In doing so, the context that there may or may not be a value is maintained, and the ability to define a fallback value (if it’s a None) is left to the final caller.


This is to avoid retrieving the underlying value from the Option, working with it, putting it into an Option later on, retrieving it again, putting it back into an Option again etc. Doing this is an anti-pattern and the methods (map, flatMap, then and others) should be used instead.


At some point though, you will need the value. Options of course provide a mechanism to retrieve the underlying value or a specified alternative if there is no underlying value through the getOrElse method.


A quick example:

const { Some, None } = require('excoptional');

// Returns Some("jackpot") or None();
const getAnOption = () => Math.random() > .5 ? 
    Some("jackpot") : 
    None();

// myOpt is either Some("jackpot") | None()
const myOpt = getAnOption();

// get the undelrying value - "jackpot" - or default 
// to the provided string - no jackpot -  if it's a 
// None.
const value = myOpt.getOrElse("no jackpot");


When you need the underlying value, always use getOrElse and provide an alternative value in case the Option is a None.

And Then…

This implementation of Option also exposes a then method which has similar behavior to Promise.then and removes having to ask and answer the question, to map or to flatMap as reviewed above.


Regardless of whether the function used to transform the underlying value returns an Option or not, the then method will work as expected and avoid creating a nested Option.


A quick example:


const { Some, None } = require('excoptional');

// Returns a string
const appendWorld = (str) => str + " world";

// Returns an Option<string>
const maybeAppendExclamationPoint = (str) => {
  
  return Math.random() > .5 ? 
    Some(str + "!") : 
    None();

};

// `then` works regardless of the type that the function passed to it returns
const myOpt = Some("hello")
    .then(appendWorld)
    .then(maybeAppendExclamationPoint)
    .log();


For either function, using then works correctly and won’t result in a nested Option.


then will always return an Option.

Promise.then always returns a promise for the same reason

Helper Methods

This library exposes several additional helper methods to provide more capability and an exceptional development experience.


Above there was a call to the log method which conveniently console logs the Option (simpler than doing console.log(myOption)) - and nicely formats the instance.


Similarly, there’s a logAndContinue method which logs the instance and returns it; extremely useful when there is a chain of method calls and you want to inspect the value at any point in that chain without breaking it apart just for debugging.


Several other methods exist that are common to several Option libraries and Options in other languages. These include isSome, isNone, filter, etc.


Take a look at the GitHub repo or NPM page for a complete listing of the available methods. Many of these methods have extensive documentation and examples demonstrating how they can be used.

Conclusion

Options make it easy to continually and safely manipulate a value (even when it doesn't exist) in a declarative and transparent way while preserving the context that the value may (or may not) exist throughout your code.


This library, excoptional, provides the best JavaScript and TypeScript support possible along with detailed documentation and examples.


It is a standalone package with 0 dependencies and 100% test coverage.


I hope you find it (and more broadly, Options) useful.