Writing Functions in R
You are still helping that weekend R study group. The same five friends, Mara, Dev, Ada, Theo and Iris, sat a quiz out of 100 and scored 58, 91, 73, 49 and 84. In Lesson 2 you wrote the grading logic, an if / else if / else that turns a score into a grade, loose in your script. It worked. But the moment you wanted to grade a second student, you copied and pasted the whole block. Now the rule lives in five different places, and if you ever change the pass mark you have to remember to fix every copy.
A function ends that. You write the rule once, give it a name, and from then on you just call it by name with whatever score you like. The widget below is the whole idea in miniature: define a function once, then call it again and again to get a value back each time.
By the end of this lesson you will be able to:
- Define a function with
function: name it, declare its arguments, and write its body - Say what a function returns (the last line, automatically) and use
return()to leave early - Call your function, pass it inputs, reuse the value it hands back, and run it across a whole vector at once
Prerequisites: you can [run R and assign with <-](R-Syntax-and-First-Objects.html), build and name a vector, and use [if / else if / else](Control-Flow-in-R.html) with comparison operators like >=.
The four parts of a function
Here is the grading rule from Lesson 2, wrapped into a function. Each lesson runs in a fresh R session, so we build the scores right here too (run this):
Read that definition slowly, because every function you ever write has these same four parts:
- A name (
grade), the label you store it under with<-, so you can call it later. - **The
functionkeyword**, which says "what follows is a function, not a value." - Arguments (
score), the inputs, listed in the round brackets. Inside the body,scoreis a name that stands for whatever value you pass in. - The body (everything in the curly braces), the work that runs each time you call it.
In maths, a function is a rule that turns an input into an output, written \(f\colon x \mapsto f(x)\): you give it the input \(x\) and it hands back the output \(f(x)\). An R function is exactly that idea made runnable. Here \(f\) is grade, the input \(x\) is a score, and the output \(f(x)\) is the grade. The widget traces that path:
Write your first function
Your turn to build one from scratch. Complete the function is_pass so that it takes a score and returns TRUE when the score is 60 or more, and FALSE otherwise. (A comparison like score >= 60 already evaluates to TRUE or FALSE, so it is all the body needs.) Fill in the body, then check. is_pass(73) should give TRUE.
Show answer
is_pass <- function(score) {
score >= 60
}
is_pass(73)
#> [1] TRUEInputs go in, one value comes out
When you write grade(91), R does two things. First it binds the argument: inside the body, score becomes 91. Then it runs the body and hands back a value. That returned value is a normal R value, so you can store it, print it, or drop it straight into other code:
What exactly comes back? By default, a function returns the value of the last expression in its body. You do not write the word "return" for the normal case, the final line's value is handed back automatically. That is why is_pass could return score >= 60 with no extra ceremony.
Sometimes you want to leave early, before reaching the last line. That is what return() is for: it stops the function immediately and hands back whatever you give it. A common use is guarding against bad input. The || below means "or", so the guard triggers when the score is below 0 or above 100:
return() only when you need to exit early, partway through the body. Both hand back exactly one value.Printing is not returning
A beginner trap hides here. cat() writes text to the console, but its own value is an invisible NULL. So a function whose body is only a cat() prints something, yet returns NULL. Run this and watch:
After that code runs, what is the value of returned?
cat() prints as a side effect but its value is NULL, and a function returns its last expression, so returned is NULL. To return the text instead, make the last line the string (or use paste0).cat() call, whose value is NULL.One function, the whole class
Here is why the wrapping was worth it. Because grade is one named rule, you can run it across every student at once instead of copying the block five times. sapply() takes a vector and a function, applies the function to each element, and collects the results:
One line grades the whole group, and the names ride along. Better still, the cut-offs now live in exactly one place: the body of grade. Change 60 to 65 there and every student is re-graded correctly, no copy to hunt down. That is the real prize of a function, one definition, one place to change, used everywhere.
Why not just grade the whole vector?
It is tempting to skip sapply() and call grade(scores) directly on the whole five-element vector. But grade decides with if, and you saw in Lesson 2 that if wants a single TRUE or FALSE. What happens when you call grade(scores)?
grade was written for one score, so its if receives five TRUE/FALSE values and halts. Apply it one element at a time with sapply(scores, grade).What a function can and cannot see
A function has its own private workspace. Any name you create inside the body lives only while the function runs, then disappears. It never leaks out into your script:
The reverse is looser, and this is the part that bites people. A function can see names from the surrounding script, so this works even though cutoff is never passed in:
cutoff that lives somewhere else, and it breaks (or worse, quietly gives wrong answers) if that name changes or is missing. The fix is simple, pass in everything the function needs as an argument:A function can take as many arguments as you like, separated by commas. (How those arguments resolve, the surrounding environments, is the subject of Lesson 5; how to give an argument a default so you can leave it out is Lesson 4.)
A two-argument function
Write a function pass_fail that takes a score and a cutoff and returns "pass" when the score reaches the cut-off, and "fail" otherwise. Use the if (...) ... else ... form you met earlier. Fill in the body, then check. pass_fail(73, 60) should give "pass"; pass_fail(40, 60) should give "fail".
Show answer
pass_fail <- function(score, cutoff) {
if (score >= cutoff) "pass" else "fail"
}
pass_fail(73, 60)
#> [1] "pass"
pass_fail(40, 60)
#> [1] "fail"References
A few authoritative, free places to take functions further:
- R for Data Science (2e), Functions - a hands-on chapter on when and how to pull repeated code into a function.
- Advanced R (2e), Functions - the rigorous treatment: a function's three parts (arguments, body, environment) and how calling really works.
- An Introduction to R: Writing your own functions - the official manual's section on defining functions.
- Hands-On Programming with R: Writing Your Own Functions - a gentle, beginner-first build of a function from the ground up.
Lesson 3 complete
You can now write your own functions. You wrapped a repeated rule into grade() and saw the four parts of every function (a name, the function keyword, arguments, and a body); you learned that a function hands back its last expression automatically, with return() reserved for leaving early; you called functions, reused the value they return, and ran one across a whole vector with sapply(); and you saw that names made inside a function stay inside, so the safe habit is to pass in everything it needs as an argument.
Next, Lesson 4: Arguments, Defaults and the Pipe. You will give arguments default values so callers can leave them out, name arguments to pass them in any order, meet the ... that lets a function accept extra arguments, and chain several functions together with the |> pipe.