Naming your own work
Writing functions of your own: arguments with defaults, the value that comes back, and handing a function to sapply, vapply and Reduce.
The last line is the answer
A function is written as name <- function(arguments) { body }. The value of the last expression in the body is what comes back, so a one-line body needs no ceremony at all. return(value) does the same thing and is worth using when the function is long enough that the reader would otherwise have to hunt for the end.
A function is a value like any other in R: you can store it, pass it to another function and return it from one. That is why so much of the language is built out of functions that take functions.
Arguments, defaults and names
Arguments go in the parentheses, and a default is written with the equals sign: function(scores, bonus = 0). A caller who says nothing about bonus gets zero; a caller who supplies a second value, or writes bonus = 2.5, overrides it.
Naming an argument when you call a function is not fussiness. It documents the call, and it lets you supply arguments out of order, which is why round(number, digits = 1) reads better than two bare values.
Applying one function to many values
sapply(values, f) runs f once per element and simplifies the answers into a vector. lapply does the same but always returns a list, which is the honest shape when the answers differ in kind. vapply(values, f, numeric(1)) asks you to state the type and length of each answer, so a surprise fails loudly instead of quietly reshaping your result.
Two more worth meeting early: do.call(sum, list(values)) calls a function with the arguments gathered into a list, and Reduce folds a vector into a single value by combining the elements two at a time, as in Reduce("+", values) for a total.
A function every way you will need one
double <- function(n) {
n * 2
}
add_bonus <- function(scores, bonus = 0) {
round(scores + bonus, 1)
}
add_bonus(c(72, 88))
add_bonus(c(72, 88), bonus = 2.5)
sapply(c(1, 2, 3), double)
vapply(c(1, 2, 3), double, numeric(1))
lapply(c(1, 2, 3), double)
Reduce("+", c(1, 2, 3))One curve, applied twice, and a verdict for everyone
Run it and check the three curves against the function body. Then change the bonus default so the plain call is curved as well, and add a score of your own to the verdict list.
Give a function a name that says what it returns, not how it works: a reader at the call site should not have to open the body. Names like pass and verdicts do that; helper2 does not, and the reader of your code is usually you in six months.
Where a function looks for a name
A function carries the environment where it was written. When it uses a name that is not one of its arguments, R looks in that environment, then outward to the global environment, and finally at the search path. That is lexical scoping, and it is why a helper defined at the top of a script is visible inside every function below it.
The other direction is local: a name created inside a function disappears when the call returns. total <- 0 inside a function does not change a total outside it, which is usually a relief. If a function really must change something outside, it has to return the new value and let the caller assign it.
Arguments, matching and failure
Arguments match by name first, then by position. line_total(units = 2, price = 24) and line_total(24, 2) mean the same thing, but the named call cannot be broken by a later change to the parameter order. R also allows partial names, which is convenient at the prompt and a liability in code; spell the name out.
A function that checks its input fails early and clearly. stop("price must not be negative") raises an error with a message the caller can act on; warning lets the call continue. Without a guard, a negative price quietly produces a negative total and the mistake travels into the report.
Defaults are evaluated inside the function, at the moment they are needed. That is why a default can refer to another argument, as in function(x, y = length(x)), and why a default that depends on the data belongs in the body rather than the signature. Keep defaults simple: a reader should be able to see the common case in the first line.
Three traps in function writing. A body whose last line is an assignment returns the value invisibly, so a call at the prompt appears to print nothing; add a final value or a return(value). if inside a function still wants one condition, so a vector argument makes it fail or warn; use ifelse or vapply. And a misspelled argument name is an error, not a new parameter — R does not quietly add it to the call.
Try these next
- Write
area(width, height = 1)and call it with one argument and with two. - Write
celsius(fahrenheit)andvapplyit over a vector of three temperatures. - Add a guard to a function that rejects a negative quantity, then catch the message with
tryCatch.
A price calculator with a default and a guard
Run it and check each line against the call that produced it. Then change the default discount to 0.1, predict which line changes, and try a negative price to see the guard speak.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
A function body holds one expression, n + n, and no return statement. What comes back?
