rm(list = ls()) # clean-up workspace

Announcement

Functions

Function components

  • All R functions have three parts:

    • the formals(), the list of arguments which controls how you can call the function

    • the body(), the code inside the function

    • the environment(), the “map” of the location of the function’s variables

f <- function(x) x^2
f
## function(x) x^2
formals(f)
## $x
body(f)
## x^2
environment(f)
## <environment: R_GlobalEnv>

Define a function

DoNothing <- function() {
  return(invisible(NULL))
}
DoNothing()

Invoke a function

mean(1:10, na.rm = TRUE)
## [1] 5.5
args <- list(1:10, na.rm = TRUE)
do.call(mean, args)
## [1] 5.5

Lexical scoping

Name masking

  • Names defined inside a function mask names defined outside a function
x <- 10
y <- 20

g02 <- function(){
   x <- 1  # a local variable to the function
   y <- 2
   c(x, y)
}
g02()
## [1] 1 2
  • If a name isn’t defined inside a function, R looks one level up.
x <- 2
g03 <- function() {
   y <- 1
   c(x, y)
}
g03()
## [1] 2 1
y
## [1] 20
  • R searches inside the current function, then looks where the function is defined and so on, all the way up to the global environment.

  • Finally, R looks in other loaded packages.

y <- 10

f <- function(x) {
   y <- 2
   y^2 + g(x)
}

g <- function(x) {
   x * y
}

What is the value of f(3)?

functions versus variables

  • In R, functions are ordinary objects. This means the scoping rules described above also apply to functions.

  • However, rules get complicated when functions and non-functions share the same name.

  • Better avoid assigning same names to objects

A fresh start

  • Every time a function is called a new environment is created to host its execution.
g11 <- function() {
  if (!exists("a")) {
    a <- 1
  } else {
    a <- a + 1
  }
  a
}

g11()
## [1] 1
g11()
## [1] 1

What happens if we do

a <- 1:5
g11()
g11()

Dynamic lookup

  • Lexical scoping determines where to look for values.

  • R looks for values when the function is run, not when the function is created.

g12 <- function() x + 1
x <- 15
g12()
## [1] 16
x <- 20
g12()
## [1] 21
  • Depending on variables defined in the global environment can be bad!

  • codetools::findGlobals() can be helpful

Default arguments

  • You can define default values for arguments

  • Default values can be in terms of other arguments, or even in terms of variables defined later in the function

  • This is because R uses Lazy Evaluation that function arguments are only evaluated if accessed.

h04 <- function(x = 1, y = x * 2, z = a + b) {
  a <- 10
  b <- 100
  
  c(x, y, z)
}

h04()
## [1]   1   2 110

... (dot-dot-dot)

  • Functions can have a special argument ...

  • With ..., a function can take any number of additional arguments

  • You can use ... to pass those additional arguments on to another function

Pro

  • If your function takes a function as an argument, you want some way to pass additional arguments to that function.
x <- list(c(1, 3, NA), c(4, NA, 6))
str(lapply(x, mean, na.rm = TRUE))   
## List of 2
##  $ : num 2
##  $ : num 5

Con

  • A misspelled argument will not raise an error.
sum(1, 2, NA, na_rm = TRUE)
## [1] NA

Control flow

These are the basic control-flow constructs of the R language. They function in much the same way as control statements in any Algol-like (Algol short for “Algorithmic Language”) language. They are all reserved words.

keyword usage
if if(cond) expr
if-else if(cond) cons.expr else alt.expr
for for(var in seq) expr
while while(cond) expr
break breaks out of a for loop
next halts the processing of the current iteration and advances the looping index

Exiting a function

Most functions exit in one of two ways:

Implicit versus explicit returns

There are two ways that a function can return a value:

  • Implicitly, where the last evaluated expression is the return value:
j01 <- function(x) {
  if (x < 10) {
    0
  } else {
    10
  }
}
j01(5)
## [1] 0
j01(15)
## [1] 10
  • Explicitly, by calling return()
j02 <- function(x) {
  if (x < 10) {
    return(0)
  } else {
    return(10)
  }
}
  • You can hide the output from automatic printing by applying invisible() to the last value:
j04 <- function() invisible(1)
j04()

Errors

If a function cannot complete its assigned task, it should throw an error with stop(), which immediately terminates the execution of the function.

j05 <- function() {
  stop("I'm an error")
  return(10)
}
j05()
## Error in j05(): I'm an error

Exit handlers

  • Use on.exit() to set up an exit handler that is run regardless of whether the function exits normally or with an error

  • Always set add = TRUE when using on.exit(). Otherwise, each call will overwrite the previous exit handler.

j06 <- function(x) {
  cat("Hello\n")
  on.exit(cat("Goodbye!\n"), add = TRUE)
  
  if (x) {
    return(10)
  } else {
    stop("Error")
  }
}

j06(TRUE)
## Hello
## Goodbye!
## [1] 10
j06(FALSE)
## Hello
## Error in j06(FALSE): Error
## Goodbye!
  • Can use exit handler for clean-up
with_dir <- function(dir, code) {
  old <- setwd(dir)
  on.exit(setwd(old), add = TRUE)

  code
}

getwd()
## [1] "/Users/xji3/Dropbox/My_Files/Tulane/Teaching/tulane-math-7360-2021.github.io/lectures/07-Functions"
with_dir("~", getwd())
## [1] "/Users/xji3"
getwd()
## [1] "/Users/xji3/Dropbox/My_Files/Tulane/Teaching/tulane-math-7360-2021.github.io/lectures/07-Functions"