June 29, 2018

PFP: Sorting Things Out

Welcome to Part Two of Practical Functional Programming (PFP). To learn more about the origin of this series, read the Introduction: Practical Functional Programming: Prelude.


Problem

You add a new feature to your app and other parts break.

Example

Code: TypeScript (good)

We are building a World Cup app. The app needs to show a team leaderboard based on the number of titles they’ve won. We have an unsorted list of World Cup winners, so we write a function to sort the teams by numWorldCupTitles in descending order:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Team {
  numWorldCupTitles: number;
  country: string;
}

const teams: Array<Team> = [
  { numWorldCupTitles: 4, country: "Italy" },
  { numWorldCupTitles: 1, country: "France" },
  { numWorldCupTitles: 2, country: "Uruguay" },
  { numWorldCupTitles: 1, country: "Spain" },
  { numWorldCupTitles: 2, country: "Argentina" },
  { numWorldCupTitles: 4, country: "Germany" },
  { numWorldCupTitles: 1, country: "England" },
  { numWorldCupTitles: 5, country: "Brazil" }
];

const sortByNumTitles = (teams: Array<Team>): Array<Team> =>
  teams.sort(
    (a: Team, b: Team) =>
      b.numWorldCupTitles - a.numWorldCupTitles
  );

const teamsByNumTitles = sortByNumTitles(teams);

Somewhere else in the app, we’d like to show the top team. Since we already have the teams sorted by title wins, we pick the first team from the list…

26
27
28
29
const topTeam = teamsByNumTitles[0];
console.log(
  `Top team: ${topTeam.country} (${topTeam.numWorldCupTitles})`
);

…and get the correct result:

Top team: Brazil (5)

Later on, a section of our app needs to show the list of World Cup winners in alphabetical order:

Code: TypeScript (bad)

We write a function to sort the teams by country:

23
24
25
26
const sortByCountry = (teams: Array<Team>) =>
  teams.sort((a: Team, b: Team) =>
    a.country.localeCompare(b.country)
  );

We pass the teams constant into sortByCountry and store it in const teamsByCountry next to the previously set const teamsByTitles:

28
29
const teamsByNumTitles = sortByNumTitles(teams);
const teamsByCountry = sortByCountry(teams);

However, when we run the program right after adding the new code above, without making any other changes, our top team has changed and is wrong:

Top team: Argentina (2)

Cause

Many operations in JavaScript mutate their arguments, among them Array::sort, Object.assign, delete, Date::setDate, etc. Note: There are exceptions such as Array::map, Array::filter, Math.abs, String::toLowerCase, etc. (the last two because Number and String are immutable)

In our case, we pass in const teams to both sortByNumTitles and sortByCountry which use Array::sort under the hood and therefore mutate teams, despite it being declared as const.

Ultimately, this means even when we only introduce new code without changing existing one, we can run into regressions due to side-effects of mutation.

Background: const

In JavaScript, const declarations can be misleading because they only guarantee that they are never reassigned, not that their underlying data is left untouched:

const numbers = [1, 2, 3];

// OK: Mutation
console.log("Reversed numbers:", numbers.reverse());
console.log("Original numbers:", numbers);
// Reversed numbers: [3, 2, 1]
// Original numbers: [3, 2, 1] // Unexpected!

// Not OK: Reassignment
numbers = [5, 4, 6];
// TypeError: Assignment to constant variable.

Solution

What if we eliminated mutation as a core mechanism in the language we use? What if there was no difference between immutable primitive types such as string, number, and boolean and mutable ones such as Array, Object, etc.?

That’s exactly what PureScript and most other functional programming languages do.

Let’s see what that looks like in practice:

Code: PureScript (good)

We start out with the same functionality as the first TypeScript example:

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type Team = { numWorldCupTitles :: Int, country :: String }

teams :: Array Team
teams = [
  { numWorldCupTitles: 4, country: "Italy" },
  { numWorldCupTitles: 1, country: "France" },
  { numWorldCupTitles: 2, country: "Uruguay" },
  { numWorldCupTitles: 1, country: "Spain" },
  { numWorldCupTitles: 2, country: "Argentina" },
  { numWorldCupTitles: 4, country: "Germany" },
  { numWorldCupTitles: 1, country: "England" },
  { numWorldCupTitles: 5, country: "Brazil" }
]

sortByNumTitles :: Array Team -> Array Team
sortByNumTitles ts = Array.sortBy
  (\a b -> compare b.numWorldCupTitles a.numWorldCupTitles) ts

main :: forall eff. Eff (console :: CONSOLE | eff) Unit
main = do
  let teamsByNumTitles = sortByNumTitles teams
      maybeTopTeam = teamsByNumTitles !! 0
  case maybeTopTeam of
    Just topTeam ->
      Console.log ("Top team: " <> topTeam.country <>
        " (" <> show topTeam.numWorldCupTitles <> ")")
    Nothing -> Console.log "No top team found."

Note: The PureScript’s array !! index translates to array[index] in JavaScript. Similarly to Array.find from Part One, it returns Maybe a to indicate that there is no result (Nothing) when we access an out of bounds index. That’s why we are reminded to handle both, the Just a and Nothing, cases.

Running the PureScript program above logs the expected result:

Top team: Brazil (5)

Next, we expand the program to create an alphabetical list of all World Cup winners just like we did with TypeScript.

Code: PureScript (still good)

We add the function to sort teams alphabetically by country:

31
32
33
sortByCountry :: Array Team -> Array Team
sortByCountry ts = Array.sortBy
  (\a b -> compare a.country b.country) ts

Then we introduce the teamsByCountry constant:

37
38
  let teamsByNumTitles = sortByNumTitles teams
      teamsByCountry = sortByCountry teams

We run the program and still get the expected result as teams was not mutated by Array.sortBy:

Top team: Brazil (5)

Conclusion

Abandon the distinction between values and references and treat everything as immutable values.

Embracing a functional programming language such as PureScript will automatically guide you to The Pit of Success, where every value is immutable by default and functions return immutable data.

This means adding new code or changing existing one will not introduce regressions caused by mutation related side-effects.


Enjoyed this post? Follow me on Twitter @gasi to learn when the next one is out.


Notes

  • In JavaScript and TypeScript, we can prevent the error above by first manually creating a shallow copy of the input array using Array::slice before calling Array::sort on it, but this takes some experience and diligence to do every time:

Code: TypeScript (fixed)

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const sortByNumTitles = (teams: Array<Team>): Array<Team> =>
  teams.slice().sort(
//      ^^^^^^^
    (a: Team, b: Team) =>
      b.numWorldCupTitles - a.numWorldCupTitles
  );

const sortByCountry = (teams: Array<Team>) =>
  teams.slice().sort((a: Team, b: Team) =>
//      ^^^^^^^
    a.country.localeCompare(b.country)
  );

const teamsByNumTitles = sortByNumTitles(teams);
const teamsByCountry = sortByCountry(teams);

With this change, the correct result is logged:

Top team: Brazil (5)
  • An amazing side-effect (no pun intended) of treating everything as value is that PureScript avoids some famous JavaScript Wat moments:

Code: JavaScript

Due to the difference between value types (string, number, boolean, etc.) and reference types (Array, Object, etc.), equality in JavaScript is not always intuitive:

$ node

// Equality works as expected…
> 1 === 1
true

> true === true
true

> "hello" === "hello"
true

// …until it doesn’t:
> [] === []
false

> [1, 2] === [1, 2]
false

> {} === {}
false

> {a: "b"} === {a: "b"}
false

Code: PureScript 0.12

PureScript, treating all types as values, makes equality intuitive.

Note: PureScript doesn’t implicitly coerce types and therefore only needs a single equality operator, namely ==.

$ pulp repl

> 1 == 1
true

> true == true
true

> "hello" == "hello"
true

> [] == [] :: Array Number
true

> [1, 2] == [1, 2]
true

> {} == {}
true

> {a: "b"} == {a: "b"}
true