23 Principles of functional programming


Summary

  • The tidyverse is a collection of packages in R.

  • The tidyverse package is written in the style of an API, that is, it is a collection of functions designed to connect to other hardware or software. In the case of the tidyverse, the functions are centered around manipulation of data stored in tibbles or R data frames.

  • One of the main principles of the tidyverse is the embrace of functional programming principles.

  • Functional programming has principles including making programming functions like mathematical functions, immutability of variables, and the ability to pass functions to other functions.

  • The tidyverse also encourages using functions like words to build sentences. That way functions only need to accomplish narrow tasks, and the overall system retains flexibility.


23.1 Application programming interface

An API (application programming interface) is a collection of functions and tools that allow the creation of applications that access another piece of software or service.

For instance, there are APIs for

  • accessing the operating system;

  • accessing a graphics card;

  • accessing a hard drive.

Now, in technical terms the tidyverse is a collection of packages. But it is written as an API in that it creates a collection of functions designed to interact with tibbles. These functions share a common convention on names, behavior, and what kind of coding structure is encouraged.

The purpose of an API is to make life easier for the programmer who creates code, the updater who maintains the code, and the end user who receives the output of the code.

In the case of the tidyverse, Hadley Wickham had four principles in mind when creating the packages that comprise the system.

  1. It should reuse existing data structures.

  2. It should compose simple functions with pipes to make more complicated constructs instead of creating separate functions for every possible need.

  3. The API should embrace functional programming.

  4. It should be designed with humans in mind.

Let’s look at each of these principles in turn.

23.2 Reusing existing structures

Data has been collected for millennia. While a census count of France in the 17th century might only be of interest to historians, data from ten or even a hundred years ago is often still of great importance today.

Therefore it is important that any tools work with systems for organizing information that already exist. In the context of R, that means that any new tools should work within the context of the data frame, a commonly used data type for storing data in R.

That is why the tibble, the preferred data storage form in the tidyverse, extends the data frame rather than replacing it. Any package in the tidyverse can take as an argument either a tibble or a data frame.

23.3 Pipes make code easier

No one can hold a complex series of transformations entirely in their heads. Pipes give us a semantic way of breaking down such a series into their component parts. That way we can handle large tasks one step at a time.

So what does this mean when writing new functions? There are a couple of things to keep in mind.

  • Keep functions simple. That means they should have as few inputs as possible, and return only one thing. That makes it easy to chain functions together using pipes.

  • Function names should be verbs when possible. That makes the piped code easier to read. The function filter filters out observations, select selects variables, and so on.

Of course, these are guidelines, not hard and fast rules. Most of the geom_ functions in ggplot2, for instance, are nouns rather than verbs, because they are adding a particular thing to the canvas.

23.4 Use functional programming

This is a big one, and so will take some explanation. There are several types of programming paradigms. Three of the most common are as follows.

  1. Imperative programming. Here the focus of the programmer is how to modify the state of the system in order to accomplish a task. Often commands remove an existing portion of the state and replace it with a new one. The Turing machine is the canonical example of imperative programming.

  2. Functional programming. Here the focus is on listing the transformations needed to get from the current state to the final state. Pure functional programming uses immutable variables, meaning that the value of a variable cannot be overwritten once assigned. Also, many functions are pure, meaning that given a particular input, they always return the same output. The lambda calculus is the canonical example of functional programming.

  3. Event-driven programming. Here functions are triggered by outside events. This type of programming is useful for game design or making user interfaces for data analysis.

Note that none of these paradigms is “right” or “better.” Instead, they have different strengths and weaknesses that encourage the user to think about their problem and write code to solve it in different ways.

Most procedural and object-oriented languages are imperative. On the other hand, since a statistic is a function of the data, many statistical analyses have a clearer form when written as a functional program.

So what makes a language functional? Not all functional languages are the same, but they usually have one or more of the following features.

  • Functions in the programming language are primarily mathematical functions, also known as pure functions. These functions always return the same output given the same input.

  • Variables are immutable, meaning they cannot be changed once assigned.

  • Loops do not use variable assignments to run.

  • Functions are first-class and can also be higher-order. This means you can pass functions as input to other functions.

23.4.1 Functions are mathematical functions

In imperative programming, a function can either be like a mathematical function (for example: \(y = x^2\)) or it can output different values for the same input based on the state of the system.

In a programming language, a function is pure if it always produces the same output for the same input, and if there are no side effects. No side effects means that the function does not change the value of the input variables or any global state.

In functional programming, most functions are mathematical (aka pure) functions. The following code in R incorporates the function \(y(x) = x^2\). It returns one thing, the output of the function.

y <- function(x)
  return(x^2)

Now suppose you use a global variable. By default, R allows the use of other variables inside of functions. The value of this variable can change the output of the function, even if the input stays the same!

c <- 3
y <- function(x)
  return(c + x^2)
print(y(4))
## [1] 19

Since the value of c can be changed, this can change the behavior of the function y.

c <- 5
print(y(4))
## [1] 21

Here the input 4 is the same, but the output of y is different from before. This behavior makes it very difficult to prove that a function does exactly what it is supposed to do, and leads to difficult bugs in code.

23.5 Variables are immutable

In a functional program, it is not permitted to change the value of a variable! Once you have assigned a variable, you cannot change its value.

Say that variables in a programming language are immutable if they can only be assigned once.

This is again to bring variables in line with how variable names are used in mathematics. For instance, if I write \[\begin{align*} y &= x^2 \\ y &= -|x| - 2, \end{align*}\] from a mathematical perspective, this means \(x^2 = -|x| - 2\) (which has no real solution). From a programming perspective, this overwrites the previous value of y with the last used expression, which can be a problem if changes are made to y later that the coder did not expect.

Unfortunately, R does enforce variable immutability. It happily allows you to change the values of variables. So if you are going to use this principle with R, you will have to do it yourself. That means writing code like

x <- 4
y1 <- x * x
y2 <- 3 * y1 + 2
y3 <- -y2

instead of

x <- 4
y <- x * x
y <- 3 * y + 2
y <- -y

Using immutable variables prevents you from accidentally changing the value of a variable and then expecting it to be the same as it was before. Or if you are collaborating in writing code on a large project, it prevents you from changing a variable in one part of the code that you are working on, thereby breaking code that your collaborator had finished.

23.6 Avoiding variable assignment in loops

Immutability quickly runs into an issue. One of the most common control constructions in programming languages is the loop, which executes a series of commands more than once. For instance, consider the following snippet of C code:

#include <stdio.h>

int main () {

  int a, s = 0;

  /* for loop execution */
  for( a = 10; a < 20; a = a + 1 ){
    s = s + a;
  }
  printf("%d\n",s);
  return 0;
}

Without going into the details of the C programming language, this code calculates \(\sum_{a = 10}^{19} a = 145\). It does this by keeping track of the sum at each stage of the computation, and changing the variable and at each step. So what can go wrong? Well, suppose that there was a bug in this code:

#include <stdio.h>

int main () {

  int a, s = 0;

  /* for loop execution */
  for( a = 10; a < 20; a = a + 1 ){
    s = s + a;
    a = a - 1;
  }
printf("%d\n",s);
return 0;
}

Inside the for loop, the value of a is being reduced by one at each step, so in the execution of the for loop, it undoes the addition of 1 to a. This code will never stop; it will run forever!

That’s bad! The good news is that immutable variables cannot change the values of variables once assigned, so this type of bug cannot arise in most functional languages.

But of course that raises the question of how exactly to run a for loop? There are multiple ways, but one answer is to use recursion instead.

A function is defined recursively if it refers to itself in the definition.

Consider how to build that same for loop using recursion. To do this, first make the function a bit more general. Say that \[ s(n) = \sum_{a = 10}^n a. \] Then mathematically the function \(s(n)\) can be defined recursively as follows: \[\begin{align*} s(10) &= 10 \\ s(n) &= n + s(n - 1) & & \text{when } n > 10. \end{align*}\]

In this definition, there is a base case \(s(10) = 10\) and a recursive case \(s(n) = n + s(n - 1)\). From this description, it is possible to directly build recursive code to evaluate \(s(n)\).

s <- 
  function(n) {
    if (n == 10) return(10)
    else return(n + s(n - 1))
}

Note that it was never necessary to redefine a variable in this program! Now, R does have a for loop, but it does use a safer method of iterating over a set or vector instead of manually adjusting the loop variable.

Consider the following R code:

for (i in 1:5) {
  i <- i - 1
  print(i)
}
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4

Because the for loop is written using the set of numbers 1:5 rather than incrementing a loop counter, the infinite-loop bug of the earlier C code example cannot occur.

Moreover, the tidyverse package purrr has functions such as map that allow the application of a function over each element of a vector in order to accomplish tasks that for loops are often called upon to do. Use of these types of functions is both safer and leads to more readable code in the long run.

23.7 Functions are first-class and higher order

In R, the assignment operator <- is used to assign the function to a particular name. This name can then be used to pass the function as an input to other functions.

In a programming language, a function is first-class if it is treated like any other object bound to a variable name.

Since it is treated like any other object, functions can be passed as input to another function, and return functions as results. Functions that do this are higher-order.

A function which takes a function as input or returns a function as output is called higher-order.

Recall that the optim function in R takes as input a function that is being optimized. This is an example of a higher-order function.

23.8 Functional programming and data science

That is functional programming in a nutshell. So how does that relate to R and data science?

  • R itself is not a fully functional language, but it incorporates enough features of functional languages that it is possible to employ many of the principles of functional programming. Sticking to this paradigm is very helpful both for code readability and in large collaborative projects.

  • Functional programming fits in very nicely with the data science view that we are transforming our data to make patterns obvious. Fully functional languages such as Haskell are sometimes used in data science when correctness is of paramount importance.

23.9 Designing the API for humans

The last principle for the design of the tidyverse is that it will be used by humans. Previous chapters have not talked much about computational complexity. That is partially because that would lead us deeper into the particular algorithms for accomplishing data science tasks than is needed here, but also because in practice most of the difficulty of data analysis comes from the time spent by humans and not the time spent by the computer. (This is not a hard and fast rule, and as seen earlier with the conditional inference forest some algorithms do take a significant amount of time.)

Therefore, it is essential that you make your analysis as transparent as possible to humans, sometimes even at the cost of making the code slower.

This also informs the choice of function names. For instance, the geometry functions all begin with geom_. This makes them easier for people to remember, and also has the added benefit of making autocomplete more powerful, as a user can scroll through a set of possibilities in order to decide what is appropriate.

In naming your functions, do not be afraid to have a lengthy name if the descriptive power of the name is needed. Save short names for functions that will be used very often, and then overall your code will be much easier to read and use by others.