Vectorised operations, calling and writing functions

Published

November 5, 2025

In this session, we will learn how to use R’s vectorised operations to write short and efficient code, and how to call and write functions.

Vectorized operations

Introduction

We often need to apply the same operation to many values, for example to all values stored in a vector. In many programming languages, this is done element by element in a loop. Here is an example in JavaScript (C or Java look very similar):

for (i = 0; i < length; i++) {
  z[i] = x[i] + y[i];
}

The loop starts with i at 0, adds the i-th elements of x and y, stores the result in z[i], increases i by 1, and repeats this until i reaches length.

In R, we write instead:

z <- x + y

This is a vectorised operation: an operation that is applied to a whole vector at once, instead of to one value at a time. Vectorised operations are:

  • shorter and easier to read: one line instead of a loop;
  • faster: the loop still happens, but inside R’s internal compiled code instead of in R code.

R is built around vectors:

  • most objects are vectors or are built from vectors: a single number is a vector of length one, and a matrix is a vector with dimensions;
  • most basic operations (+, -, *, /, ^, comparisons such as >) and many functions (e.g. sqrt(), log()) are vectorised;
  • if two vectors in an operation have different lengths, the shorter one is recycled (repeated) to match the longer one (see the R documentation about recycling).

So before writing a loop in R, check whether a vectorised solution exists.

NoteSide note

Loops in R are not as slow as they used to be (since R 3.4, R code is automatically compiled to byte code). Vectorised code is still faster and usually easier to read, but a loop is perfectly fine when an operation cannot be vectorised.

Simple mathematical operations

Vector and single number

Arithmetic operators (+, -, *, /, ^, %% for modulo, …) work on whole vectors. In many other languages, we need a loop to add 1 to each element of a vector. In Python, for example:

x = [0, 1, 2, 3, 4, 5]
x = [item + 1 for item in x]
x
# [1, 2, 3, 4, 5, 6]

In R, we simply write:

x <- 0:5
x + 1
[1] 1 2 3 4 5 6

What happens here:

  1. x + 1 adds a vector of length 6 (x) and a vector of length 1 (1).
  2. R recycles the shorter vector to the length of the longer one: 1 becomes c(1, 1, 1, 1, 1, 1).
  3. The two vectors are added element-wise: first element to first element, second to second, and so on.

Two vectors

Two vectors of the same length are combined element by element:

y <- 5:0
z <- x + y
z
[1] 5 5 5 5 5 5

The \(i\)-th element of z is the sum of the \(i\)-th elements of x and y: \(z_i = x_i + y_i\).

Can you predict and explain the results in the following exercises?

Exercise 1

r <- c(1,1,1,1)
s <- c(1,3)
r + s
[1] 2 4 2 4

R recycles the shorter vector s to the length of r and adds the vectors element-wise: c(1,1,1,1) + c(1,3) is the same as c(1,1,1,1) + c(1,3,1,3).

Exercise 2

v <- c(1,2,3)
r + v
Warning in r + v: longer object length is not a multiple of shorter object
length
[1] 2 3 4 2

R still recycles v (to c(1,2,3,1)), but because the length of r (4) is not a multiple of the length of v (3), it gives a warning. Such a warning usually points to a mistake in the code.

WarningSilent recycling

R only warns when the longer length is not a multiple of the shorter one. If it is, e.g. 1:4 + 1:2, R recycles silently, even if this was not intended. Tibbles and dplyr functions are stricter: they only recycle vectors of length 1 and give an error otherwise.

Exercise 3

r + 5L
[1] 6 6 6 6

r is a numeric vector and 5L is an integer (the suffix L creates an integer). R converts the integer to numeric and recycles it as before. Compare class(r) and class(5L) with class(r + 5L).

Matrix and single number

A matrix is a vector with two dimensions (rows and columns). Its values can be accessed with row and column coordinates, or with their position in the underlying vector. The length of a matrix is the number of rows times the number of columns:

m <- matrix(11:16, nrow = 2, ncol = 3)
m            # printed as a matrix
     [,1] [,2] [,3]
[1,]   11   13   15
[2,]   12   14   16
length(m)    # 2 rows x 3 columns = 6
[1] 6
m[2, 2]      # value in row 2, column 2
[1] 14
m[4]         # the same value, by its position in the underlying vector
[1] 14
as.vector(m) # the underlying vector
[1] 11 12 13 14 15 16

Note that matrices are filled column by column by default (use byrow = TRUE to fill them row by row).

Because a matrix is a vector, the same recycling rules apply. A single number is recycled to all elements of the matrix:

m1 <- matrix(1:4, nrow = 2, ncol = 2)
m1
     [,1] [,2]
[1,]    1    3
[2,]    2    4
m1 + 1 # same as m1 + matrix(1, nrow = 2, ncol = 2)
     [,1] [,2]
[1,]    2    4
[2,]    3    5

Matrix and vector

A vector is recycled along the underlying vector of the matrix, i.e. column by column:

m1 + 1:2 # adds 1 to the first row and 2 to the second row
     [,1] [,2]
[1,]    2    4
[2,]    4    6
m1 + 1:4 # adds 1:4 in the order in which the matrix was filled
     [,1] [,2]
[1,]    2    6
[2,]    4    8

Exercise 4

What happens here?

m1 + 1:3
Warning in m1 + 1:3: longer object length is not a multiple of shorter
object length
     [,1] [,2]
[1,]    2    6
[2,]    4    5

R recycles 1:3 and adds it element-wise. Since 4 is not a multiple of 3, it gives a warning.

If the vector is longer than the matrix, R gives an error instead:

m1 + 1:8
Error:
! dims [product 4] do not match the length of object [8]

Two matrices

Two matrices with the same dimensions are combined element-wise:

m2 <- matrix(5:8, nrow = 2, ncol = 2)
m1 + m2
     [,1] [,2]
[1,]    6   10
[2,]    8   12

Exercise 5

What happens here?

m3 <- matrix(1:6, nrow = 3, ncol = 2)
m1 + m3
Error in `m1 + m3`:
! non-conformable arrays

Matrices are not recycled: all matrices in an operation must have the same dimensions (dim()), otherwise R gives an error.

Vectorised functions

Many functions are vectorised as well. It helps to distinguish two kinds:

  • element-wise functions return one value per element, e.g. sqrt(), log(), round(), abs(), and comparisons such as > or ==;
  • summary functions return a single value for the whole vector, e.g. sum(), mean(), max(), length().
vals <- c(1, 4, 9, 16)
sqrt(vals) # one result per element
[1] 1 2 3 4
vals > 5   # one TRUE/FALSE per element
[1] FALSE FALSE  TRUE  TRUE
sum(vals)  # one result for the whole vector
[1] 30
mean(vals)
[1] 7.5

Linear algebra

Element-wise multiplication

* multiplies two matrices element-wise, just as + adds them. The matrices must have the same dimensions.

m1 * m2
     [,1] [,2]
[1,]    5   21
[2,]   12   32

This is not the matrix product known from linear algebra (see below).

Matrix product of two matrices

The matrix product is calculated with the %*% operator:

\[ C_{ij} = \sum_{k=1}^n A_{ik} B_{kj} \]

Each element of the result is obtained by multiplying a row of the first matrix with a column of the second matrix, element by element, and summing up the products. Therefore, the number of columns of the first matrix must equal the number of rows of the second matrix.

From https://en.wikipedia.org/wiki/Matrix_multiplication

Example:

m1 %*% m2
     [,1] [,2]
[1,]   23   31
[2,]   34   46

Step by step:

Element Row of m1 × column of m2 Result
top-left (1 × 5) + (3 × 6) 23
top-right (1 × 7) + (3 × 8) 31
bottom-left (2 × 5) + (4 × 6) 34
bottom-right (2 × 7) + (4 × 8) 46

Matrix products are used in many statistical methods, for example in linear models.

Matrix product of a matrix and a vector

A vector can also be multiplied with a matrix. R treats the vector as a row or a column, depending on its side:

  • vector on the left: a row vector, its length must equal the number of rows of the matrix;
  • vector on the right: a column vector, its length must equal the number of columns of the matrix.
m3 <- matrix(1:6, nrow = 3, ncol = 2)
m3
     [,1] [,2]
[1,]    1    4
[2,]    2    5
[3,]    3    6
c(1,2,3) %*% m3
     [,1] [,2]
[1,]   14   32
m3 %*% c(2,2)
     [,1]
[1,]   10
[2,]   14
[3,]   18

More exercises

Exercise 6

Convert the temperatures -20°C, 0°C and 100°C to degrees Fahrenheit using vectorised arithmetic.

Hint: \(°F = °C \times 9/5 + 32\).

x <- c(-20, 0, 100)
(x * 9/5) + 32
[1]  -4  32 212

Exercise 7

Given the matrix m <- matrix(1:20, nrow = 4, ncol = 5), what are the results of:

  1. m ^ 0.5
  2. m * c(0,1,2,3)
  3. m > c(100, 0)
  4. m * m
  5. m %*% m
m <- matrix(1:20, nrow = 4, ncol = 5)
# a)
m ^ 0.5 # or sqrt(m)
         [,1]     [,2]     [,3]     [,4]     [,5]
[1,] 1.000000 2.236068 3.000000 3.605551 4.123106
[2,] 1.414214 2.449490 3.162278 3.741657 4.242641
[3,] 1.732051 2.645751 3.316625 3.872983 4.358899
[4,] 2.000000 2.828427 3.464102 4.000000 4.472136
# b)
m * c(0,1,2,3)
     [,1] [,2] [,3] [,4] [,5]
[1,]    0    0    0    0    0
[2,]    2    6   10   14   18
[3,]    6   14   22   30   38
[4,]   12   24   36   48   60
# c)
m > c(100, 0)
      [,1]  [,2]  [,3]  [,4]  [,5]
[1,] FALSE FALSE FALSE FALSE FALSE
[2,]  TRUE  TRUE  TRUE  TRUE  TRUE
[3,] FALSE FALSE FALSE FALSE FALSE
[4,]  TRUE  TRUE  TRUE  TRUE  TRUE
# d)
m * m # or m^2
     [,1] [,2] [,3] [,4] [,5]
[1,]    1   25   81  169  289
[2,]    4   36  100  196  324
[3,]    9   49  121  225  361
[4,]   16   64  144  256  400
# e)
# m %*% m # error: non-conformable arguments (4x5 times 4x5)
t(m) %*% m # works after transposing the first matrix (5x4 times 4x5)
     [,1] [,2] [,3] [,4] [,5]
[1,]   30   70  110  150  190
[2,]   70  174  278  382  486
[3,]  110  278  446  614  782
[4,]  150  382  614  846 1078
[5,]  190  486  782 1078 1374

Functions

Introduction

A function is a piece of R code stored in an object of class "function". It takes input via arguments (e.g. numbers, vectors, data frames), processes it, and returns an output (e.g. a value, a table, a plot).

Many functions come with base R, and many more with packages. Most of them are written in R themselves. To see their code, type the function name without parentheses:

sd # R's implementation of the standard deviation
function (x, na.rm = FALSE) 
sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
    na.rm = na.rm))
<bytecode: 0x10cfcaaa0>
<environment: namespace:stats>

You can also write your own functions. This keeps your code readable, avoids copy-pasting the same code several times, and makes debugging easier.

R is a functional programming language: functions are its central building blocks, and they can be handled like any other object (see R as a functional language).

Calling functions

You call a function by writing its name followed by the arguments in round parentheses:

function_name(argument1, argument2, ...)

For example, rnorm() draws random numbers from a normal distribution. args() lists its arguments (the NULL at the end can be ignored):

args(rnorm)
function (n, mean = 0, sd = 1) 
NULL
x <- rnorm(1000) # 1000 random numbers with mean 0 and sd 1

Arguments

There are two kinds of arguments:

  • mandatory arguments have no default value and must be provided, e.g. n in rnorm();
  • optional arguments have a default value that is used when you do not provide one, e.g. mean = 0 and sd = 1 in rnorm().
rnorm(3)                     # uses the defaults mean = 0 and sd = 1
[1] -0.7308592  0.0390541  1.7182840
rnorm(3, mean = 100, sd = 5)
[1]  96.69213  97.65073 104.05563

When you call a function, R matches the values to the arguments either by name or by position. Matching by position is convenient, but easy to get wrong. sd() has the arguments x and na.rm, so the following call is matched as sd(x = TRUE, na.rm = x) and fails:

sd(TRUE, x) # incorrect: the first argument is `x`, the second `na.rm`
Error in `if (na.rm) ...`:
! the condition has length > 1

With names, the order does not matter. R first matches all named arguments, and then the remaining ones by position:

sd(na.rm = TRUE, x = x)
[1] 1.025266
sd(na.rm = TRUE, x)
[1] 1.025266
sd(TRUE, x = x)
[1] 1.025266

All of these work, but they are hard to read. A good convention is to pass the main (first) argument by position and all other arguments by name:

sd(x, na.rm = TRUE)
[1] 1.025266

Function structure

Code structure

You create a function with function() and assign it to a name:

my_function <- function(arg1, arg2) {
  statements
  return(object)
}
  • the arguments are listed in the round parentheses;
  • the code that is run (the body) goes into the curly brackets;
  • return() defines the output.

For example:

square <- function(x) {
  x^2
}
square(1:5)
[1]  1  4  9 16 25

Since x^2 is vectorised, square() is vectorised as well.

Make sure that the last expression really is the value you want to return. In the function below, the last expression is an assignment. The value of an assignment is its right-hand side, returned invisibly (i.e. not printed). So the function returns the names, not the named vector:

make_scores <- function() {
  scores <- c(12, 15, 9)
  names(scores) <- c("Anna", "Ben", "Cleo")
}
make_scores()        # prints nothing
res <- make_scores()
res                  # the names, not the named vector!
[1] "Anna" "Ben"  "Cleo"

To fix this, end the function with the object you want to return:

make_scores <- function() {
  scores <- c(12, 15, 9)
  names(scores) <- c("Anna", "Ben", "Cleo")
  scores
}
make_scores()
Anna  Ben Cleo 
  12   15    9 

Exercise 8

Write a function that adds two vectors a and b element-wise.

add <- function(a, b) {
  a + b
}
add(1:3, 1:3)
[1] 2 4 6

Naming conventions

Function names follow the same rules as other object names: they must start with a letter and should be descriptive. Since functions do something, a verb often makes a good name, e.g. convert_temperature().

Functions can also be anonymous, i.e. without a name (called “lambda functions” in other languages). They are useful for short functions that are passed to other functions, which we will see in a later session. Since R 4.1, there is a shorthand for them: \(x) x^2 is the same as function(x) x^2.

Internal parts of a function

A function written in R has three parts:

  • formals(): the list of arguments (args() shows them in a more readable form);
  • body(): the code run by the function;
  • environment(): the environment in which the function was defined. This is where R looks for variables that are not defined inside the function (see Environments and variable scoping).
formals(square)
$x
body(square)
{
    x^2
}
environment(square) # defined in the global environment
<environment: R_GlobalEnv>

The same works for functions from base R and packages:

args(sd)
function (x, na.rm = FALSE) 
NULL
body(sd)
sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
    na.rm = na.rm))
environment(sd) # defined in the stats package
<environment: namespace:stats>

Since these parts are ordinary R objects, they can also be changed. As a (contrived) example, we create a copy of sd() that removes NAs by default:

my_sd <- sd
formals(my_sd)$na.rm <- TRUE # change the default value of na.rm
args(my_sd)
function (x, na.rm = TRUE) 
NULL
my_sd(c(1, 2, NA))
[1] 0.7071068
sd(c(1, 2, NA))              # the original sd() is unchanged
[1] NA

The body can be replaced in the same way:

cube <- square
body(cube) <- quote(x^3) # quote() stores code without running it
cube(2)
[1] 8
square(2)                # the original square() is unchanged
[1] 4

This shows how functions are built, but in practice you would simply write a new function, e.g. my_sd <- function(x) sd(x, na.rm = TRUE).

Primitive functions

A few basic functions, such as sum(), sin() or [, are primitive: for efficiency, they directly call internal C code and have no formals, body or environment:

sum
function (..., na.rm = FALSE)  .Primitive("sum")
formals(sum)
NULL
body(sum)
NULL
environment(sum)
NULL

Operators such as +, [ or <- are also functions. The usual way of writing them (1 + 2, x[2], y <- 5) is syntactic sugar: a more convenient notation that R translates into a regular function call. Enclosed in backticks, they can be called like any other function:

`+`(1, 2)  # same as 1 + 2
[1] 3
x <- c(5, 6, 7)
`[`(x, 2)  # same as x[2]
[1] 6
`<-`(y, 5) # same as y <- 5
y
[1] 5

Even if, for and { are functions in R.

This is different from many other languages. In C, operators are part of the language syntax: they are not functions, and they cannot be called by name or redefined. In Python, a + b is translated into a call of a special method of the object (a.__add__(b)), but + itself is not an object that you can call or reassign (a function version exists separately as operator.add). In R, + is an ordinary object, so it can even be redefined (never do this in real code!):

`+` <- function(e1, e2) e1 - e2
1 + 2
[1] -1
rm(`+`) # remove our version, the original is visible again
1 + 2
[1] 3

You can also define your own infix operators. Their names must start and end with %, like the built-in %*% or %in%:

`%+%` <- function(a, b) paste(a, b)
"Hello" %+% "world"
[1] "Hello world"

Dot-dot-dot argument ...

The special argument ... (dot-dot-dot, or ellipsis) accepts any number of arguments, named or unnamed. This is how functions such as c() or paste() accept any number of inputs. Here is an illustrative example:

count_args <- function(...) {
  arglist <- list(...)
  message("you called me using ", length(arglist), " arguments")
}
count_args()
you called me using 0 arguments
count_args(1:10)
you called me using 1 arguments
count_args(1, 2, 3, 4, 5)
you called me using 5 arguments

... collects all arguments that could not be matched to other arguments by name or by position.

A common use of ... is to pass arguments on to another function:

mean_rounded <- function(x, ...) {
  round(mean(x, ...), 1) # `...` is passed on to mean()
}
mean_rounded(c(1.234, 5.678, NA), na.rm = TRUE)
[1] 3.5

Exercise 9

How many arguments end up in ... in the following calls?

count_args(1:2)
count_args(1, 2)
count_args(1:2, 3:4)
count_args(1:4)
  • count_args(1:2): 1 (a single vector with two elements)
  • count_args(1, 2): 2 (two length-one vectors)
  • count_args(1:2, 3:4): 2 (two length-two vectors)
  • count_args(1:4): 1 (a single vector with four elements)

R as a functional language

R is a functional programming language: programs are built mainly by writing and combining functions. Two ideas from functional programming are particularly useful.

Functions are first-class objects

Functions are objects like any other (numbers, vectors, data frames). They can be:

my_max <- max # assign a function to a new name
my_max(c(3, 8, 1))
[1] 8
summaries <- list(min = min, max = max, mean = mean) # functions in a list
summaries$mean(c(3, 8, 1))
[1] 4
apply_fun <- function(f, x) { # `f` is a function passed as an argument
  f(x)
}
apply_fun(min, c(3, 8, 1))
[1] 1
apply_fun(\(v) v * 2, c(3, 8, 1)) # an anonymous function as an argument
[1]  6 16  2

Passing functions to other functions is very common in R: for example, lapply() and sapply() apply a given function to each element of a list or vector.

Pure functions

In functional programming, functions should ideally be pure:

  1. the output depends only on the inputs, so the same inputs always give the same output;
  2. the function has no side effects: it does not change anything outside of itself (global variables, files, plots, …).

R does not enforce this, but supports it. When a function modifies an argument, it modifies a local copy (copy-on-modify), and the original object stays unchanged:

double_first <- function(v) {
  v[1] <- v[1] * 2
  v
}
nums <- c(1, 2, 3)
double_first(nums)
[1] 2 2 3
nums # unchanged
[1] 1 2 3

Many R functions are not pure, often for good reasons: rnorm() returns different values on every call, and print(), plot() or write.csv() are used precisely for their side effects. Still, writing your own functions to be as pure as possible makes them predictable and easy to test and reuse. In particular, do not use or change global variables inside your functions (see Exercise 12).

Environments and variable scoping

An environment is a place in memory where R stores objects (values, data sets, functions, …). When you work in the console or in a script, you work in the global environment (shown in the Environment pane in RStudio).

The scope of a variable is the part of the code in which the variable is visible. Every time a function is called, R creates a new environment for it. Arguments and variables created inside a function exist only in this environment:

# no myarg in the global environment
exists("myarg")
[1] FALSE
# but myarg exists inside of myfunction
myfunction <- function(myarg = 1) {
  return(exists("myarg"))
}
myfunction()
[1] TRUE

A function can, however, use variables defined outside of it. Which variable it finds is determined by R’s lookup rules below.

Where R looks for variables (lexical scoping)

When a function uses a variable, R looks for it:

  1. in the function’s own environment (its arguments and the variables created inside it);
  2. if it is not there, in the environment in which the function was defined (for functions created in the console or in a script, this is the global environment);
  3. then in the loaded packages (this is how R finds e.g. sum() or sd());
  4. if the variable is not found anywhere, R throws an error.

Variables defined inside the function are found first:

f <- function() {
  x <- 1
  y <- 2
  c(x, y)
}
f()
[1] 1 2
rm(f) # clean-up

Variables that are not defined inside the function are looked up in the global environment:

x <- 2
g <- function() {
  y <- 1
  c(x, y) # x is defined in the global environment
}
g()
[1] 2 1
rm(x, g) # clean-up

This is a common source of bugs: if you forget to define a variable inside a function, the function silently uses an object with the same name from the global environment, and its result depends on what happens to be there. Therefore, pass all inputs as arguments, and test your functions in a fresh R session (in RStudio: Session > Restart R). This is especially important when you move code from a script into a function, because the objects created by the script are still in the global environment.

When R looks for variables (dynamic lookup)

R looks up a variable when the function is called, not when it is defined. The function therefore uses the value that the variable has at the time of the call:

x <- 2
g <- function() {
  y <- 1
  c(x, y)
}
x <- 3
g() # uses the value of `x` at the time of the call
[1] 3 1
rm(x, g)

A fresh start with every function call

Each call creates a new, empty environment, so a function does not remember anything from previous calls:

counter <- function() {
  if (!exists("a")) {
    a <- 1     # `a` does not exist yet
  } else {
    a <- a + 1 # would run if `a` was remembered from a previous call
  }
  a
}
counter()
[1] 1
counter() # still 1: `a` from the first call is gone
[1] 1

A function can create and return another function. The returned function is called a closure, because it encloses the environment in which it was created: it keeps access to the variables of that environment, even after the outer function has finished.

A typical use is a function factory, a function that creates other functions:

make_power <- function(exp) {
  function(x) {
    x^exp # `exp` is found in the environment created by make_power()
  }
}
power2 <- make_power(2)
power3 <- make_power(3)
power2(1:4)
[1]  1  4  9 16
power3(1:4)
[1]  1  8 27 64

Each call of make_power() creates a new environment with its own exp, which the returned function remembers.

Closures can also keep a state between calls. Each call of the counter below still gets a fresh environment, but count lives in the enclosing environment created by make_counter(), which persists. The super-assignment operator <<- modifies count there, instead of creating a local copy:

make_counter <- function() {
  count <- 0
  function() {
    count <<- count + 1
    count
  }
}
counter_a <- make_counter()
counter_a()
[1] 1
counter_a()
[1] 2
counter_b <- make_counter() # a new, independent counter
counter_b()
[1] 1
counter_a()
[1] 3

Same name for a function and a variable

If you create an object with a name that already exists in the global environment, the old object is overwritten. Functions from base R and packages live in their own environments, so they are not overwritten but masked: R finds your object first. When you remove your object, the original function is visible again.

When you call a function (name followed by ()), R skips objects that are not functions:

sum           # the function
function (..., na.rm = FALSE)  .Primitive("sum")
sum <- 15
sum           # the variable masks the function
[1] 15
sum(c(1,2,3)) # calling sum() still works
[1] 6
rm(sum)       # removing the variable...
sum           # ...unmasks the function
function (..., na.rm = FALSE)  .Primitive("sum")

Still, avoid names such as sum, mean, c or data for your own objects, as they make code confusing.

More exercises

Exercise 10a

What is the output of sum(1,2,3,4,5)?

sum(1,2,3,4,5)
[1] 15

Exercise 10b

What is the output of mean(1,2,3,4,5)?

mean(1,2,3,4,5)
[1] 1

Exercise 10c

What is the reason for the results of the two previous exercises?

Check the help pages of both functions:

?sum
?mean

sum() has the arguments ... and na.rm. All unnamed arguments end up in ... and are summed up, even a logical value (TRUE counts as 1). To set na.rm, it must be named:

sum(1,2,3,4,5, TRUE)
[1] 16
sum(1,2,3,4,5, na.rm = TRUE)
[1] 15

mean() calculates the mean of its first argument x only. The other values are matched by position to the arguments trim and na.rm, or end up unused in ..., without any error or warning. So mean(1,2,3,4,5) returns the mean of 1.

Exercise 10d

How can we get the intended results?

Provide the numbers as a single vector.

sum(c(1,2,3,4,5))
[1] 15
mean(c(1,2,3,4,5))
[1] 3

Exercise 11

Write a function C_to_F() that converts temperatures from degrees Celsius to degrees Fahrenheit (see Exercise 6).

Bonus: write the reverse function F_to_C() and check that F_to_C(C_to_F(x)) returns x.

C_to_F <- function(x) {
  (x * 9/5) + 32
}
C_to_F(c(-20, 0, 100))
[1]  -4  32 212
F_to_C <- function(x) {
  (x - 32) * 5/9
}
F_to_C(C_to_F(c(-20, 0, 100)))
[1] -20   0 100

Exercise 12

Can you predict what the code below does? Can you explain the result?

gb_val <- 10
edit_global_val <- function(x) {
  gb_val <- gb_val + x
  return(gb_val)
}
# apply the edit...
edit_global_val(5)
# ...but did the global value actually change?
gb_val
[1] 15
[1] 10

The function returns 15, but the global gb_val is still 10. The assignment gb_val <- gb_val + x inside the function reads the global gb_val (lexical scoping), but stores the result in a new, local gb_val in the function’s environment. This local copy is discarded after the call.

It is possible to change a global variable from within a function with the super-assignment operator <<-, which assigns to the variable in the environment where the function was defined:

gb_val <- 10
edit_global_val <- function(x) {
  gb_val <<- gb_val + x
  return(gb_val)
}
edit_global_val(5)
[1] 15
gb_val
[1] 15

However, this is rarely a good idea. Rule of thumb: do not change objects outside a function from within the function. Instead, return the result and assign it explicitly, e.g. gb_val <- add(gb_val, 5).