7.2 Functions in R

Functions are your loyal servants, waiting patiently to do your bidding to the best of their ability. They’re made with the utmost care and attention … though sometimes may end up being something of a Frankenstein’s monster - with an extra limb or two and a head put on backwards. But no matter how ugly they may be they’re completely faithful to you.

They’re also very stupid.

If we asked you to go to the supermarket to get us some ingredients to make Francesinha, even if you don’t know what the heck that is, you’d be able to guess and bring at least something back. Or you could decide to make something else. Or you could ask a celebrity chef for help. Or you could pull out your phone and search online for what Francesinha is. The point is, even if we didn’t give you enough information to do the task, you’re intelligent enough to, at the very least, try to find a work around.

If instead, we asked our loyal function to do the same, it would listen intently to our request, stand still for a few milliseconds, compose itself, and then start shouting Error: 'data' must be a data frame, or other object .... It would then repeat this every single time we asked it to do the job. The point here, is that code and functions are not intelligent. They cannot find workarounds. It’s totally reliant on you, to tell it very explicitly what it needs to do step by step.

Remember two things: the intelligence of code comes from the coder, not the computer and functions need exact instructions to work.

To prevent functions from being too stupid you must provide the information the function needs in order for it to function. As with the Francesinha example, if we’d supplied a recipe list to the function, it would have managed just fine. We call this “fulfilling an argument”. The vast majority of functions require the user to fulfill at least one argument.

This can be illustrated in the pseudocode below. When we make a function we can specify what arguments the user must fulfill (e.g. argument1 and argument2), as well as what to do once it has this information (expression):

nameOfFunction <- function(argument1, argument2, ...) {expression}

The first thing to note is that we’ve used the function function() to create a new function called nameOfFunction. To walk through the above code; we’re creating a function called nameOfFunction. Within the round brackets we specify what information (i.e. arguments) the function requires to run (as many or as few as needed). These arguments are then passed to the expression part of the function. The expression can be any valid R command or set of R commands and is usually contained between a pair of braces { } (if a function is only one line long you can omit the braces). Once you run the above code, you can then use your new function by typing:

nameOfFunction(argument1, argument2)

Confused? Let’s work through an example to help clear things up.

First we are going to create a data frame called city, where columns porto, aberdeen, nairobi, and genoa are filled with 100 random values drawn from a bag (using the rnorm() function to draw random values from a Normal distribution with mean 0 and standard deviation of 1). We also include a “problem”, for us to solve later, by including 10 NA values within the nairobi column (using rep(NA, 10)).

city <- data.frame(
  porto = rnorm(100),
  aberdeen = rnorm(100),
  nairobi = c(rep(NA, 10), rnorm(90)),
  genoa = rnorm(100)
)

Let’s say that you want to multiply the values in the variables Porto and Aberdeen and create a new object called porto_aberdeen. We can do this “by hand” using:

porto_aberdeen <- city$porto * city$aberdeen

We’ve now created an object called porto_aberdeen by multiplying the vectors city$porto and city$aberdeen. Simple. If this was all we needed to do, we can stop here. R works with vectors, so doing these kinds of operations in R is actually much simpler than other programming languages, where this type of code might require loops (we say that R is a vectorised language). Something to keep in mind for later is that doing these kinds of operations with loops can be much slower compared to vectorisation.

But what if we want to repeat this multiplication many times? Let’s say we wanted to multiply columns porto and aberdeen, aberdeen and genoa, and nairobi and genoa. In this case we could copy and paste the code, replacing the relevant information.

porto_aberdeen <- city$porto * city$aberdeen
aberdeen_genoa <- city$aberdeen * city$aberdeen
nairobi_genoa <- city$nairobi * city$genoa

While this approach works, it’s easy to make mistakes. In fact, here we’ve “forgotten” to change aberdeen to genoa in the second line of code when copying and pasting. This is where writing a function comes in handy. If we were to write this as a function, there is only one source of potential error (within the function itself) instead of many copy-pasted lines of code (which we also cut down on by using a function).

In this case, we’re using some fairly trivial code where it’s maybe hard to make a genuine mistake. But what if we increased the complexity?

city$porto * city$aberdeen / city$porto + (city$porto * 10^(city$aberdeen)) 
                  - city$aberdeen - (city$porto * sqrt(city$aberdeen + 10))

Now imagine having to copy and paste this three times, and in each case having to change the porto and aberdeen variables (especially if we had to do it more than three times).

What we could do instead is generalise our code for x and y columns instead of naming specific cities. If we did this, we could recycle the x * y code. Whenever we wanted to multiple columns together, we assign a city to either x or y. We’ll assign the multiplication to the objects porto_aberdeen and aberdeen_nairobi so we can come back to them later.

# Assign x and y values
x <- city$porto
y <- city$aberdeen

# Use multiplication code
porto_aberdeen <- x * y

# Assign new x and y values
x <- city$aberdeen
y <- city$nairobi

# Reuse multiplication code
aberdeen_nairobi <- x * y

This is essentially what a function does. OK down to business, let’s call our new function multiply_columns() and define it with two arguments, x and y. In the function code we simply return the value of x * y using the return() function. Using the return() function is not strictly necessary in this example as R will automatically return the value of the last line of code in our function. We include it here to make this explicit.

multiply_columns <- function(x, y) {
  return(x * y)
}

Now that we’ve defined our function we can use it. Let’s use the function to multiple the columns city$porto and city$aberdeen and assign the result to a new object called porto_aberdeen_func

porto_aberdeen_func <- multiply_columns(x = city$porto, y = city$aberdeen)
porto_aberdeen_func
##   [1]  0.160213167 -0.522138296 -0.095578928  0.262724717  0.214014973
##   [6] -1.787407712 -0.026538909 -0.877030447 -0.026985220 -0.059183405
##  [11] -2.793419895 -0.470698760  2.183006145  2.733317870  0.273346309
##  [16] -0.789937672  0.290814698 -0.252801059 -0.571025963 -0.401293430
##  [21] -1.286818960 -1.329882359 -0.529618143 -0.279247531 -0.280445116
##  [26] -0.511893567  0.060616675  0.279929620  0.568601070 -4.184967336
##  [31]  0.408965586  2.159449881 -0.195985495 -0.008835835 -2.307311478
##  [36]  0.010726697 -0.841836813  0.152310018 -2.382656818  0.350689876
##  [41] -0.504212225 -1.282243754 -0.146663601  1.072774051 -0.001072562
##  [46] -0.170586411  1.301184448  0.972548930  0.084966033 -0.151913220
##  [51]  0.057257910 -0.047441391  1.535557180 -0.182348207 -0.344256897
##  [56]  0.045409296  0.135118818 -0.253897870 -0.455315767  0.091530525
##  [61] -0.120765287 -1.294787208 -0.169825813 -0.511847722 -0.013162223
##  [66]  1.258515514  0.283929056 -0.222826895  0.640684938 -0.079518570
##  [71]  0.085790039  0.015222388  0.883052844  0.427146533 -0.238443065
##  [76] -0.083778444 -0.482075568  2.100846197 -0.144946167 -0.700433802
##  [81]  0.648693258 -2.132014780  2.068205913  0.335519404  0.395332432
##  [86]  0.165488049  0.214874915  1.175049755 -0.063313507 -0.362213981
##  [91] -1.810896295  0.197666580 -1.079902941 -0.009615856  1.101724701
##  [96]  0.455589109 -0.380696406 -0.816197676  0.462645350  0.469055401

If we’re only interested in multiplying city$porto and city$aberdeen, it would be overkill to create a function to do something once. However, the benefit of creating a function is that we now have that function added to our environment which we can use as often as we like. We also have the code to create the function, meaning we can use it in completely new projects, reducing the amount of code that has to be written (and retested) from scratch each time. As a rule of thumb, you should consider writing a function whenever you’ve copied and pasted a block of code more than twice.

To satisfy ourselves that the function has worked properly, we can compare the porto_aberdeen variable with our new variable porto_aberdeen_func using the identical() function. The identical() function tests whether two objects are exactly identical and returns either a TRUE or FALSE value. Use ?identical if you want to know more about this function.

identical(porto_aberdeen, porto_aberdeen_func)
## [1] TRUE

And we confirm that the function has produced the same result as when we do the calculation manually. We recommend getting into a habit of checking that the function you’ve created works the way you think it has.

Now let’s use our multiply_columns() function to multiply columns aberdeen and nairobi. Notice now that argument x is given the value city$aberdeenand y the value city$nairobi.

aberdeen_nairobi_func <- multiply_columns(x = city$aberdeen, y = city$nairobi)
aberdeen_nairobi_func
##   [1]           NA           NA           NA           NA           NA
##   [6]           NA           NA           NA           NA           NA
##  [11] -0.195098155  0.017819867  2.173426057 -0.416220721  0.873544769
##  [16] -0.958354386 -0.741163700  0.030776236 -0.547571182  0.924713372
##  [21] -0.098298363 -0.134410808  0.045669036  0.596996660 -1.614073750
##  [26] -0.614713031 -0.123671521 -0.011518735 -2.119691553 -0.640301261
##  [31] -0.547699813  0.278470916 -0.193678551  0.019699547  1.204434150
##  [36]  0.006017941  0.012818194 -0.287228679  0.095943373  1.075442962
##  [41]  0.806511514 -3.477053162 -0.086686004  0.623551011 -0.716543544
##  [46]  1.678053717  0.269023469  0.343956064  0.593243875 -0.143538379
##  [51] -0.008223864 -0.002561936 -1.259816833  0.057136490  1.219620340
##  [56] -0.079354508 -0.052123265  0.226414104  0.436379510 -0.225763270
##  [61]  0.276812856  1.453425419 -0.049092643 -1.620873468 -0.041519802
##  [66]  1.739615388 -0.460159870  0.125595999  0.061961886  0.103242193
##  [71]  0.699027216 -0.090440286  0.722336385 -0.075854354  0.012131337
##  [76]  0.006565262  0.237943935  0.428934414  0.235042850 -1.422420436
##  [81]  0.580418459 -0.648547101  0.371873349  1.127917969 -3.043118168
##  [86]  0.197816017 -0.485597959 -1.144604717 -0.470867206  0.147545229
##  [91] -0.005258398 -0.203437861 -1.353549090 -0.506529421 -1.135714253
##  [96]  0.378215778 -0.603558273  0.538841442 -0.667775602 -0.414839144

So far so good. All we’ve really done is wrapped the code x * y into a function, where we ask the user to specify what their x and y variables are.

Now let’s add a little complexity. If you look at the output of nairobi_genoa some of the calculations have produced NA values. This is because of those NA values we included in nairobi when we created the city data frame. Despite these NA values, the function appeared to have worked but it gave us no indication that there might be a problem. In such cases we may prefer if it had warned us that something was wrong. How can we get the function to let us know when NA values are produced? Here’s one way.

multiply_columns <- function(x, y) {
  temp_var <- x * y
  if (any(is.na(temp_var))) {
    warning("The function has produced NAs")
    return(temp_var)
  } else {
    return(temp_var)
  }
}

aberdeen_nairobi_func <- multiply_columns(city$aberdeen, city$nairobi)
## Warning in multiply_columns(city$aberdeen, city$nairobi): The function has
## produced NAs
porto_aberdeen_func <- multiply_columns(city$porto, city$aberdeen)

The core of our function is still the same. We still have x * y, but we’ve now got an extra six lines of code. Namely, we’ve included some conditional statements, if and else, to test whether any NAs have been produced and if they have we display a warning message to the user. The next section of this Chapter will explain how these work and how to use them.