21  Writing Functions

Figure 21.1: A common consideration when writing a new function: how long will it take to write vs. how much time will it save you in the long run. (Only an issue really for long and complex functions.)

Writing functions is a core part of programming.

When should you write a function?

-> Whenever you find yourself repeating pieces of code.

Why is it important?

-> Writing functions helps reduce the total amount of code, which increases efficiency, reduces the chances of error, and can make code more readable.

Functions in R are “first-class objects”.

This means they can be stored inside other objects (e.g. a list), they can be passed as arguments to other functions (as we see in Chapter 24) and can be returned as output from functions.

Functions in R are for the most part like mathematical functions: they have one or more inputs and one output. The inputs are known as the function arguments. If you want to return multiple outputs, you can return a list containing any number of R objects.

Functions are referred to as “closures” in R. A closure is made of a function and its environment. Closures are distinct from primitive functions (i.e. internally implemented / built-in functions, which are written in C).

21.1 Simple functions

Let’s start with a very simple function: single argument with no default value:

Define the function:

square <- function(x) {
  x^2
}

Try our new function:

square(4)
[1] 16

Notice above that x^2 is automatically returned by the function. It is the same as explicitly returning it with return():

square <- function(x) {
  return(x^2)
}

square(4)
[1] 16

also same:

square <- function(x) {
  out <- x^2
  return(out)
}

square(4)
[1] 16

still same:

square <- function(x) {
  out <- x^2
  out
}

square(5)
[1] 25

A function returns either:

  • the value of the last expression within the function definition such as out or x^2 above.
  • an object passed to return().

Multiple arguments, with and without defaults:

raise <- function(x, power = 2) {
  x^power
}

x <- sample(10, size = 1)
x
[1] 10
raise(x)
[1] 100
raise(x, power = 3)
[1] 1000
raise(x, 3)
[1] 1000
Tip

return() can be used to exit a function early

In the following example, return() is used to exit the function early if no negative values are found. This is shown only as a trivial example; it is not particularly useful in this case, but can be useful in more complex functions.

preproc <- function(x) {
  if (all(x >= 0)) {
    return(x) 
  } else {
    cat("Negative values found, returning absolute \n")
    return(abs(x))
  }
}

The following stops early and no message is printed:

preproc(0:10)
 [1]  0  1  2  3  4  5  6  7  8  9 10

The following does not stop early and message is printed:

preproc(-5:5)
Negative values found, returning absolute 
 [1] 5 4 3 2 1 0 1 2 3 4 5

21.2 Argument matching

R will match unambiguous abbreviations of arguments:

fn <- function(alpha = 2, beta = 3, gamma = 4) {
  alpha * beta + gamma
}
fn(g = 2)
[1] 8

21.3 Arguments with prescribed set of allowed values

You can match specific values for an argument using match.arg():

myfn <- function(type = c("alpha", "beta", "gamma")) {
  type <- match.arg(type)
  cat("You have selected type '", type, "'\n", sep = "")
}

myfn("a")
You have selected type 'alpha'
myfn("b")
You have selected type 'beta'
myfn("g")
You have selected type 'gamma'
myfn("d")
Error in match.arg(type): 'arg' should be one of "alpha", "beta", "gamma"

Above you see that partial matching using match.arg() was able to identify a valid option, and when there was no match, an informative error was printed.

Partial matching is also automatically done on the argument names themselves, but it’s important to avoid depending on that.

adsr <- function(attack = 100,
                 decay = 250,
                 sustain = 40,
                 release = 1000) {
  cat("Attack time:", attack, "ms\n",
      "Decay time:", decay, "ms\n",
      "Sustain level:", sustain, "\n",
      "Release time:", release, "ms\n")
}

adsr(50, s = 100, r = 500)
Attack time: 50 ms
 Decay time: 250 ms
 Sustain level: 100 
 Release time: 500 ms

21.4 Passing extra arguments to another function with the ... argument

Many functions include a ... argument at the end. Any arguments not otherwise matched are collected there. A common use for this is to pass them to another function:

cplot <- function(x, y,
                  cex = 1.5,
                  pch = 16,
                  col = "#18A3AC",
                  bty = "n", ...) {

  plot(x, y, 
       cex = cex, 
       pch = pch, 
       col = col, 
       bty = bty, ...)

}

... is also used for variable number of inputs, often as the first argument of a function. For example, look at the documentation of c(), cat(), cbind(), paste().

Note

Any arguments after the ..., must be named fully, i.e. will not be partially matched.

21.5 Return multiple objects

R function can only return a single object. This is not much of a problem because you can simply put any collection of objects into a list and return it:

lfn <- function(x, fn = square) {
  xfn <- fn(x)
  
  list(x = x,
       xfn = xfn,
       fn = fn)
}

lfn(3)
$x
[1] 3

$xfn
[1] 9

$fn
function(x) {
  out <- x^2
  out
}
<bytecode: 0x10df82e38>

21.6 Warnings and errors

You can use warning("some warning message") at any point inside a function to produce a warning message during execution. The message gets printed to the R console, but function execution is not stopped.

On the other hand, you can use stop("some error message") to print an error message to console and stop function execution.

The following function (el10) calculates: \[ e^{log_{10}(x)} \]

el10 <- function(x) {
  exp(log10(x))
}

which is not defined for negative x. In this case, we could let R give a warning when it tries to compute log10(x):

val1 <- el10(-3)

We could instead produce our own warning message:

el10 <- function(x) {
  if (x < 0) warning("x must be positive")
  exp(log10(x))
}
val2 <- el10(-3)
val2
[1] NaN

As you see, the output (NaN) still gets returned.

Alternatively, we can use stop() to end function execution:

el10 <- function(x) {
  if (x < 0) stop("x must be positive")
  exp(log10(x))
}
val3 <- el10(-3)
Error in el10(-3): x must be positive

Note how, in this case, function evaluation is stopped and no value is returned.

21.7 Documenting Functions

It is essential to document every function you write, especially if you plan to share it with. The roxygen2 allows you to write special inline comments that can generate complete documentation for your functions. Visit the link to read its documentation.

Tip

Make it a habit to document your functions as you write them. As you write more & more complex functions, it becomes harder and more time-consuming to document them later. After all, a function with incomplete or no documentation is of little use.