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 intosortByCountry
and store it inconst teamsByCountry
next to the previously setconst 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:
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 toarray[index]
in JavaScript. Similarly toArray.find
from Part One, it returnsMaybe 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, theJust a
andNothing
, 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) tsThen we introduce the
teamsByCountry
constant:
37 38 let teamsByNumTitles = sortByNumTitles teams teamsByCountry = sortByCountry teamsWe run the program and still get the expected result as
teams
was not mutated byArray.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 callingArray::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