Chapter 1. What is functional programming?

We construct our programs using only pure functions - functions that have no side effects.

A function has a side effect if it does anything other than return a value. Examples:

  • Modifying any state, such as a variable or data structure, in place
  • Throwing an exception or exiting the program with an error
  • Reading or writing a file
  • Making a network request

Benefits of FP: a simple example

We'll walk through refactoring a simple program to remove side effects and demonstrate some TypeScript syntax. We'll also touch on two import concepts in functional programming: referential transparency and the substitution model.

See the code repository for expanded, runnable versions of these examples.

A program with side effects

// `class` keyword introduces a class
class Cafe {

  // method of a class introduced by a
  // name followed by ()
  buyCoffee(cc: CreditCard): Coffee {   // `cc: CreditCard` defines parameter
                                        //  named `cc` of type `CreditCard`
                                        //
                                        // `: Coffee` declares the return type
                                        // of the method. compiler will error
                                        // if the method doesn't return a
                                        // `Coffee` object

    const cup = new Coffee();
    cc.charge(cup.price);               // side effect: charges the card
    return cup;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

The line cc.charge(cup.price) is an example of a side effect. Charging a credit card involves some interaction with the outside world. But the function's return value is just a Coffee, meaning this interaction is not easily observable, making our function difficult to test. We can improve modularity and testability by introducing a Payments object that encapsulates the payment processing logic and removing it from CreditCard.

Adding a Payments object

class Cafe {
  buyCoffee(cc: CreditCard, p: Payments): Coffee {
    const cup = new Coffee();
    p.charge(cc, cup.price);
    return cup;
  }
}
1
2
3
4
5
6
7

The side effect still happens. But we have improved testability because we can pass in a mock Payments object. However, any mock will be awkward to use, because it will have to do things like maintain internal state that we can inspect after the call to charge(). This is a bit much if all we want is to test that buyCoffee charges the correct amount for a cup of coffee. It's also going to be tough to reuse buyCoffee. Say we wanted to buy 10 coffees: there is no obvious way to do that without contacting the payment processor 10 times!

A functional solution: removing the side effects

// buyCoffee now returns a pair, or tuple, of the
// purchased Coffee  and its associated Charge
buyCoffee(cc: CreditCard): [Coffee, Charge] {
  const cup = new Coffee();
  const charge = new Charge(cc, cup.price);
  return [cup, charge];
}
1
2
3
4
5
6
7

Here, we've removed the side effect. Instead of immediately interacting with the payment processor, buyCoffee returns a new Charge value object along with the Coffee. We can think of this as a description of what we want to happen, rather than detailed instructions on how to accomplish it. Actually interpreting the meaning of Charge objects is now a concern for elsewhere. In fact, Cafe no longer has any knowledge of how the process of charging the card works. Let's look at Charge more closely:

class Charge {
  readonly cc: CreditCard;    // once a Charge is created, it should never
  readonly amount: number;    // change, hence the `readonly` markers

  constructor(cc: CreditCard, amount: number) {
    this.cc = cc;
    this.amount = amount;
  }

  /**
   * Returns a new Charge containing the sum of the amounts of this Charge
   * and the other Charge
   **/
  combine(other: Charge): Charge {
    if (this.cc == other.cc)
      return new Charge(this.cc, this.amount + other.amount);
    else
      throw new Error("Can't combine charges to different cards");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Charge is an immutable value object, equipped with a combine() function to merge two charges into one. Now we have a way to more easily represent the idea of purchasing 10 coffees. We just need to combine the 10 charges into one.

buyCoffees(cc: CreditCard, n: number): [Coffee[], Charge] {
  const cards: CreditCard[] = new Array(n).fill(cc);
  const purchases = Array.from(cards, cc => this.buyCoffee(cc));

  // this part is a bit ugly, but we're just splitting the array of
  // [Coffee, Charge] tuples into one Coffee array and one Charge array
  const [coffs, chgs] = purchases.reduce(
    ([coffees, charges], [coffee, charge]) => {
        coffees.push(coffee);
        charges.push(charge);
        return [coffees, charges];
    },
    [new Array(), new Array()]
  );

  // reduce the list of Charges to one by sequentially applying combine()
  return [coffs, chgs.reduce((l, r) => l.combine(r))];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Our functional solution has significant advantages over the previous two iterations. It's easier to test, since all we need to do is assert that the Charge objects have the expected values in their cc and amount properties. It's also easier to combine simple, low-level behavior into more advanced functionality. Look how straightforward it was to implement batch charging! Finally, we can imagine that the library for communicating with the payment processor is made simpler by this approach. It only needs to issue the correct commands for any given value of Charge.

Note

If you've been following along in Functional Programming in Scala, you may have noticed that the Scala code corresponding to these snippets is more compact and elegant. For instance, TypeScript doesn't have the notion of case classes, requiring us to write a little more boilerplate to achieve the same end. It also lacks an unzip for easily separating a sequence of tuples into a tuple of sequences. In general, TypeScript's standard library (which is really just JavaScript's) is less comprehensive than Scala's.

On the other hand, TypeScript has better destructuring support (e.g. const [coffees, charges] = ...) and is a relatively thin enhancement to JavaScript, giving it great applicability to web programming. Many people find the experience of developing software with Node.js to be a joy (and, conversely, developing with anything JVM-based to be a constant time-suck).

Later on, we'll have to employ some advanced, potentially difficult-to-understand techniques to get the same expressive power from TypeScript's types as we can with Scala's. Still, TypeScript is a powerful, easy-to-deploy, and rapidly-evolving language that drastically improves our ability to manage and maintain large JavaScript projects.

Definition of a pure function

The book differentiates between "functions" and "procedures", stating that the phrase "pure function" is redundant. But given the preponderance of using "function" to mean any semi-cohesive, addressable sequence of code, we'll stick with explicitly calling functions "pure" when it matters. Consider a function f, with an input type of A and an output type of B. In both Scala and TypeScript, the type of f is written as A => B. Then f is pure if it relates every value of A to exactly one value of B, the output value is determined solely by the input value, and f takes no other actions that change the meaning of the program.

Some examples of pure functions:

  • Integer addition
  • Index of substring in string, if the string is immutable

The concept of referential transparency, which is a property of expressions, formalizes purity. Any part of a program that can be evaluated to a result is an expression (meaning that a function is one kind of expression), and it is referentially transparent (or RT) if its every occurence in a program can be replaced by its result without altering the meaning of the program. More formally:

Referential transparency and purity

An expression e is referentially transparent if, for all programs p, all occurrences of e in p can be replaced by the result of evaluating e without changing the meaning of p. A function f is pure if the expression f(x) is referentially transparent for all referentially transparent x.

Referential transparency allows us to reason about programs using the substitution model, wherein we discover the meaning of program by repeatedly replacing expressions with their results.

Last Updated: 4/20/2020, 7:46:22 AM