3 Comments

Experiments in Purely Functional TypeScript

Recently, I’ve been experimenting with using functional programming in my side projects. Today, I want to share some of what I’ve learned, focusing on utilities I’ve created to facilitate purely functional TypeScript programming.

Different Ways to Define Similar Functions

Here are three versions of *roughly* the same function:


  const doubleAll = (array: number[]) => {
    for (let i = 0; i < array.length; i++) {
      array[i] = array[i] * 2;
    }
    return output;
  }

  const doubleAll = (array: number[]) => {
  return array.map(x => {
    return x * 2
  });
}

  const doubleAll = (array: number[]) =>
  array.map(x => x * 2);

Note that the first function mutates the array argument. The second and third functions are roughly equivalent, but the second uses the additional curly bracket and return syntax.

My interest is in converting code from the format of the second function to that of the third.

First though, let's define a few things.

Functional Programming Basics

Here's the core of functional programming:

First-class functions

  • Functions can be passed as arguments to other functions.
  • Functions may be returned from other functions.

Pure function

  • Functions do not produce side effects.
  • The same input always produces the same output.

You're probably already using functional programming if you use map, reduce array functions, or Redux or Immutable.js.

A Subtle Distinction

Now let's look at two similar functions:


const a = (x: string) => {
  return {
    message: x;
  };
};
const b = (x: string) => ({
  message: x
});

Note that the output of both functions is the same. The difference I'm interested in, though, is that, with its outer curly brackets, `a` indicates that it will allow imperative code. What if we prohibited that and relied on exclusively descriptive code instead?

Challenges

When attempting to use non-imperative code, I ran into a handful of problems (though we can remedy them all later).

  • If/else statements aren't expressions.
  • Switch statements aren't expressions.
  • I wasn't sure how to handle intermediary values.
  • You can write some pretty ugly/unreadable nested function calls.

Intermediary values

Check out the following functions:


const efficient = (array: string[]) => {
    const complexResult = reallyComplexFunction(array);
    return 4 * complexResult + 3 * complexResult;
}
const inefficient = (array: string[]) =>
    4 * reallyComplexFunction(array) + 3 * reallyComplexFunction(array);

If we assume `reallyComplexFunction` takes a while to execute, the first (imperative) version looks great, while the second (declarative) version looks pretty ineffiecient.

Unreadable code

As mentioned above, we can also get into the habit of writing some deeply nested function calls.


const manySteps = (array: string[]) =>
    round(average(
        multiplyAll(doubleAll(
            roundAll(halveAll(incrementAll(convertToInt(array)))))));

That's pretty hard to read. There are a ton of nested function calls. Oh, and there's also a syntax error involving the wrong number of parens, but it's pretty hard to tell.

Luckily, we can do a lot better.

A Few Utility Functions

Types


  export type Lazy = () => T;
  // these are cool too
  export type Fn(t: T) => U;
  export type ById = { [key: string]: T };

We'll need Lazy for the rest of the post, but I consider the other two invaluable, so I've included them.

Arrays


  export const head = (array: T[]) =>
    array[0];
  export const tail = (array: T[]) =>
    array.slice(1);

These give us the basics of manipulating arrays that you'd see in recursive calls.

elseIf


    // definition
    const ifElse = (expr: boolean, t: Lazy, f: Lazy) =>
      expr ? t() : f();

    // use
    const evenOrOdd = (x: number) =>
      ifElse(x % 2 === 0,
        () => 'even',
        () => 'odd');
  

This is the basis for branching without if/else statements.

Range

Let's use the above implementation of ifElse to define something similar to Python's range function.


    const range = (x: number): number[] =>
    ifElse(x <= 0,
      () => [],
      () => [...range(x - 1), x - 1]);
  

Conditions

At some point, we will want the equivalent of a switch statement. I've defined mine so that it can be tested like this:


    const x = 4;
    expect(cond([
      [x === 3, () => 'nope'],
      [x < 2,   () => 'nope'],
      [x > 1,   () => 'yup'],
      [x === 4, () => 'nope']
    ])).toEqual('yup');
  

The implementation relies on a bit more that I want to include in this post, so I'll include a link to my repo at the bottom of this post.

Chaining

I love Lodash's chain, but last time I used it, it wasn't type-safe, which was a deal-breaker for me (all due respect to the library given–it's fantastic otherwise). Here's what a functional, type-safe version might look like:


const mapping = chain('14.4')
  .map(str => parseInt(str, 10))
  .map(Math.floor)
  .map(x => x * 2)
  .map(x => ({ answer: x }))
  .value();
// { answer: 28 }

Finally

All right, so we looked at a bunch of functional utilities; let's put them together to make something *slightly* complex:
Here's functional FizzBuzz:


const fizzBuzz = (max: number) =>
  range(max)
    .map((x: number) => cond([
      [!(x % 3 || x % 5), () => 'fizzBuzz'],
      [!(x % 3), () => 'fizz'],
      [!(x % 5), () => 'buzz'],
      [true, () => `${x}`]
    ]));

Of course, FizzBuzz is not the most complex function we can create, but it's certainly a step above doubleAll.

Sample Code

As promised, here's the repo with sample code!

More Refinements to Come

The biggest issue I've come across with this approach is that the type-narrowing provided by TypeScript's switch statements is not easily converted to functional code. I hope to improve this in the future.