21 Writing 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
orx^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
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.
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()
.
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)} \]
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:
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.
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.