12.2 Essentials of writing R functions

12.2.1 Basics

The basic structure for defining a new function f_name():

f_name <- function(<args>) {<body>}
  • The function’s arguments <args> specify the inputs (or the data, represented as R objects) accepted by a function.
    Each argument can be mandatory or optional (by providing defaults).

  • The function’s <body> typically uses the inputs provided by <args> to perform the task for which the function is created. It can contain arbitrary amounts of R code (including references to existing R objects and other functions). By default, the function returns the result of its last expression.

An example function:

power <- function(x, exp = 1) {
  x^exp
}

Note that the power() function contains two arguments:

  1. x is mandatory, as no default is specified in the function definition.

  2. exp is optional, as the default value of 1 is used when no other value is provided when calling the function.

Example calls:

power(x = 2)
#> [1] 2
power(x = 2, exp = 2)
#> [1] 4
power(2, 3)            # arguments omitted
#> [1] 8
power(exp = 2, x = 3)  # arguments reversed
#> [1] 9

# Note: 
power(c(1, 2, 3), 2)
#> [1] 1 4 9
power(2, c(1, 2, 3))
#> [1] 2 4 8
power(c(1, 2, 3), c(1, 2, 3))
#> [1]  1  4 27

Other checks:

power(NA)
power("A")

Guidance on evaluating functions:

  • First check the intended functionality. Which data types and data structure does it accept?

  • Then try testing the function’s limits (e.g., extreme, unusual, or missing inputs). Where does it break?

12.2.2 Advanced aspects of functions

Steps towards writing more complicated functions:

  • The structure of function body
  • Adding return() statements
  • Side effects and ... arguments
  • Issues of style

Most function bodies are more complex. Explicate the structure of function <body>:

- Prepare: Initialize variables and check inputs, 
- Main part(s), 
- Output: `return()` results. 

Explicit return()

Illustrate cases:

  1. Add an input check (as a conditional):
power_2a <- function(x, exp = 1) {
  
  # check inputs:
  if (!is.numeric(x)){
    message("Please note: x should be numeric.")
  }
  
  # main:
  x^exp
  
}

power_2a("A")

Note: Main part of function is still executed.

  1. Adding premature exit/early return():
# (b)
power_2b <- function(x, exp = 1) {
  
  # check inputs:
  if (!is.numeric(x)){
    message("Please note: x should be numeric.")
    return(paste0("You entered x = ", x))  # stops the function execution/exits the function
  }
  
  # main:
  x^exp
  
}

power_2b("A")  # stops when if is TRUE
#> [1] "You entered x = A"

Note: Premature exit when if is TRUE.

  1. Adding explicit exit/final return():

# (c) Structure: 3 explicit parts; 
power_2c <- function(x, exp = 1) {
  
  # A. prepare: 
  
  # check inputs:
  if (!is.numeric(x)){
    message("Please note: x should be numeric.")
    return(paste0("You entered x = ", x))  # stops the function execution/exits the function
  }
  
  # initialize variables: 
  output <- NA  # "something"
  
  # B. main:
  output <- x^exp
  
  # C. return the result: 
  return(output)
  
  # output  # (would also work)
  
}

power_2c("A")  # stops earlier
#> [1] "You entered x = A"
power_2c(2)    # output is returned
#> [1] 2
  1. Question: What happens when omitting the final return?
# (c) 3 explicit parts; 
power_2d <- function(x, exp = 1) {
  
  # A. prepare: 
  # initialize variables: 
  output <- NA  # "something"
  
  # check inputs:
  if (!is.numeric(x)){
    message("Please note: x should be numeric.")
    return(paste0("You entered x = ", x))  # stops the function execution/exits the function
  }
  
  # B. main:
  output <- x^exp
  
  # C. return the result: 
  # return(output)
  
}

power_2d(2)  # nothing is returned!

Note: Nothing is returned!