Want to help out or contribute?

If you find any typos, errors, or places where the text may be improved, please let us know by providing feedback either in the feedback survey (given during class) or by using GitHub.

On GitHub open an issue or submit a pull request by clicking the " Edit this page" link at the side of this page.

6  Save time, don’t repeat yourself: Making functions

Here we will cover the second block, “Workflow” in Figure 7.1.

Figure 6.1: Section of the overall workflow we will be covering.

6.1 Learning objectives

The overall learning outcome for this session is to:

  1. Describe a basic workflow for creating functions and then apply this workflow to import data.

Specific objectives are to:

  1. Describe and identify the individual components of a function as well as the workflow to creating them, and then use this workflow to make one to import some data.
  2. Describe a workflow for developing code that combines using Quarto, source()’ing with Ctrl-Shift-S or with the Palette (Ctrl-Shift-P, then type “source”), and restarting R with Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-P, then type “restart”), explain its usefulness in assessing correctness and re-usability of code, and then apply this workflow when developing functions.
  3. Explain what R package dependency management is, why it is necessary when writing code and ensuring reproducibility, and apply tools like usethis::use_package() to manage dependencies in an R project.
  4. Describe and apply a workflow of prototyping code in a Quarto document and then moving it into a script (called, e.g., functions.R) once finished and tested, then using source() to load the functions into the R session.
  5. Continue practicing Git version control to manage changes to your files.

6.2 The basics of a function

Reading task: ~5 minutes

We’ve mentioned functions multiple times, but what is a function? At its core, a function in R is anything that does an action. A function is a bundled sequence of steps that achieve a specific action. For instance, the + (to add) is a function, mean() is a function, [] (to subset or extract) is a function, and so on. In simple terms, functions are made of a function call, its arguments, and the function body:

Console
function(argument1, argument2){ 
  # body of function with R code
}

Because R is open source, anyone can see how things work underneath. So, if we want to see what a function does underneath, we type out the function name without the () into the Console and run it. If we do it with the function sd() which calculates the standard deviation, we see:

Console
sd
#> function (x, na.rm = FALSE) 
#> sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
#>     na.rm = na.rm))
#> <bytecode: 0x557c8acbc6a0>
#> <environment: namespace:stats>

Here you see sd() as the arguments x and na.rm. Within the function body is how it calculates the standard deviation, which is the square root of the variance. In this code, the var() is inside the sqrt() function, which is exactly what it should be.

So, if you learn how to create your own functions, it can make doing your work easier and more efficient because you don’t have to repeat yourself later. Making functions always has a basic structure of:

  1. Giving a name to the function (e.g. mean).
  2. Starting the function call using function(), assigning it to the name with <-. This tells R that the name is a function object.
  3. Optionally providing arguments to give to the function call, for instance function(argument1, argument2, argument3).
  4. Filling out the body of the function, with the arguments (if any) contained inside, that does some action.
  5. Optionally, use return() to indicate what you want the function to output. For learning purposes, we’ll always use return() to help show us what is the final function output.

Emphasize that we will be using this workflow for creating functions all the time throughout course and that this workflow is also what you’d use in your daily work.

While there is no minimum or maximum number of arguments you can provide for a function (e.g. you could have zero or dozens of arguments), its good practice for yourself and for others to have as few arguments as necessary to get the job done. So, the structure is:

Console
name <- function(argument1, argument2) {
    # body of function
    output <- ... code ....
    return(output)
}

Writing your own functions can be absolutely amazing and fun and powerful, but you also often want to pull your hair out with frustration at errors that are difficult to understand and fix. The best way to deal with this is by debugging. Due to time and to the challenge of making meaningful debugging exercises (solutions to problems are very dependent on the project), read Appendix C in your own time for some instructions on debugging and dealing with another common problem you might encounter with R.

Let’s write a simple example. First, create a new Markdown header called ## Making a function and create a code chunk below that with Ctrl-Alt-I or with the Palette (Ctrl-Shift-P, then type “new chunk”) . Then, inside the code chunk, we’ll write this code out:

doc/learning.qmd
add_numbers <- function(num1, num2) {
  added <- num1 + num2
  return(added)
}

You can use the new function by running the above code and writing out your new function, with arguments to give it.

doc/learning.qmd
add_numbers(1, 2)
#> [1] 3

The function name is fairly good; add_numbers is read as “add numbers”. While we generally want to write code that describes what it does by reading it, it’s also good practice to add some formal documentation to the function. Use the “Insert Roxygen Skeleton” in the “Code” menu, by typing Ctrl-Shift-Alt-R or with the Palette (Ctrl-Shift-P, then type “roxygen comment”), and you can add template documentation right above the function. It looks like:

doc/learning.qmd
#' Title
#'
#' @param num1
#' @param num2
#'
#' @return
#' @export
#'
#' @examples
add_numbers <- function(num1, num2) {
  added <- num1 + num2
  return(added)
}

In the Title area, this is where you type out a brief sentence or several words that describe the function. Creating a new paragraph below this line allows you to add a more detailed description. The other items are:

  • @param num: These lines describe what each argument (also called parameter) is for and what to give it.
  • @return: This describes what output the function gives. Is it a data.frame? A plot? What else does the output give?
  • @export: Tells R that this function should be accessible to the user of your package. Since we aren’t making packages, delete it.
  • @examples: Any lines below this are used to show examples of how to use the function. This is very useful when making packages, but not really in this case. So we’ll delete it. Let’s write out some documentation for this function:
doc/learning.qmd
#' Add two numbers together.
#'
#' @param num1 A number here.
#' @param num2 A number here.
#'
#' @return Returns the sum of the two numbers.
#'
add_numbers <- function(num1, num2) {
  added <- num1 + num2
  return(added)
}

Once we’ve created that, let’s open up the Git Interface with Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”) and add and commit these changes to our history.

6.3 Making a function for importing our data

Now that we have a basic understanding of what a function looks like, let’s apply it to something we’re doing right now: Importing our data.

Making functions is a series of steps:

  1. Write code that works and does what you want.
  2. Enclose it as a function with name <- function() { ... }, with an appropriate and descriptive name.
  3. Create arguments in the function call (function(argument1, argument2)) with appropriate and descriptive names, then replace the code with the argument names where appropriate.
  4. Rename any objects created to be more generic and include the return() function at the end to indicate what the function will output.
  5. Run the function and check that it works.
  6. Add the Roxygen documentation tags with Ctrl-Shift-Alt-R or with the Palette (Ctrl-Shift-P, then type “roxygen comment”) while the cursor is in the function.

Emphasize that we will be using this workflow for creating functions all the time throughout course and that this workflow is also what you’d use in your daily work.

In doc/learning.qmd, create a new Markdown header called ## Import the user data with a function and create a code chunk below that with Ctrl-Alt-I or with the Palette (Ctrl-Shift-P, then type “new chunk”) .

So, step one. Let’s take the code we wrote for importing the user_info data and convert that as a function:

doc/learning.qmd
user_1_info_data <- read_csv(
  user_1_info_file,
  col_select = -1,
  col_types = cols(
    gender = col_character(),
    weight = col_double(),
    height = col_double(),
    age = col_double()
  ),
  name_repair = snakecase::to_snake_case
)

Next we wrap it in the function call and give it an appropriate name. In this case, import_user_info is descriptive and meaningful.

doc/learning.qmd
import_user_info <- function() {
  user_1_info_data <- read_csv(
    user_1_info_file,
    col_select = -1,
    col_types = cols(
      gender = col_character(),
      weight = col_double(),
      height = col_double(),
      age = col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
}

Then, we add arguments in the function and replace within the code. Here, we have only one thing that we would change: The file path to the dataset. So, a good name might be file_path.

doc/learning.qmd
import_user_info <- function(file_path) {
  user_1_info_data <- read_csv(
    file_path,
    col_select = -1,
    col_types = cols(
      gender = col_character(),
      weight = col_double(),
      height = col_double(),
      age = col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
}

Then we clean things up by renaming user_1_info_data since we would like to also import more than just user_1. A nice object name would be info_data. Add the return() function at the end with the object you want your function to output.

doc/learning.qmd
import_user_info <- function(file_path) {
  info_data <- read_csv(
    file_path,
    col_select = -1,
    col_types = cols(
      gender = col_character(),
      weight = col_double(),
      height = col_double(),
      age = col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
  return(info_data)
}

Great! Now we need to test it out. Let’s try on two datasets, two user_info.csv files in the user_1 and user_2 folders.

doc/learning.qmd
import_user_info(here("data-raw/mmash/user_1/user_info.csv"))
#> # A tibble: 1 × 4
#>   gender weight height   age
#>   <chr>   <dbl>  <dbl> <dbl>
#> 1 M          65    169    29
import_user_info(here("data-raw/mmash/user_2/user_info.csv"))
#> # A tibble: 1 × 4
#>   gender weight height   age
#>   <chr>   <dbl>  <dbl> <dbl>
#> 1 M          95    183    27

Awesome! It works. The final stage is adding the Roxygen documentation.

doc/learning.qmd
#' Import MMASH user info data file.
#'
#' @param file_path Path to user info data file.
#'
#' @return Outputs a data frame/tibble.
#'
import_user_info <- function(file_path) {
  info_data <- read_csv(
    file_path,
    col_select = -1,
    col_types = cols(
      gender = col_character(),
      weight = col_double(),
      height = col_double(),
      age = col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
  return(info_data)
}

A massive advantage of using functions is that if you want to make a change to all your code, you can very easily do it by modifying the function and it will change all your other code too. Now that we have a working function, run styler with the Palette (Ctrl-Shift-P, then type “style file”) and then let’s add and commit it to the Git history with the RStudio Git Interface by using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”).

6.4 Exercise: Repeat with the saliva data

Time: 15 minutes.

Take the code you created for importing the saliva data set from Section 5.3 (not the code related to using spec()) and make it into a function. It looks like the code below.

doc/learning.qmd
user_1_saliva_data <- read_csv(
    user_1_saliva_file,
    col_select = -1,
    col_types = cols(
        samples = col_character(),
        cortisol_norm = col_double(),
        melatonin_norm = col_double()  
    ), 
    name_repair = snakecase::to_snake_case
)
Tip

A helpful tip: To move around a Quarto or R script more easily, open up the “Document Outline” on the side by clicking the button in the top right corner of the Quarto pane or by using Ctrl-Shift-O or with the Palette (Ctrl-Shift-P, then type “outline”).

  1. Create a new markdown header ## Exercise for importing the saliva data as a function.
  2. Create a new code chunk below the header with Ctrl-Alt-I or with the Palette (Ctrl-Shift-P, then type “new chunk”).
  3. Paste the code you used from the exercise (shown above) into the code chunk and begin converting it into a function, like we did above.
    • Wrap it with the function() {...}

    • Make a meaningful name (use import_saliva)

    • Make an argument for the file path (file_path) and replace user_1_saliva_file with file_path in the read_csv() code

    • Rename the output object to saliva_data and put it in the return() function

    • Run the function and then test that it works

    • Create the Roxygen documentation by adding it with

      Ctrl-Shift-Alt-R or with the Palette (Ctrl-Shift-P, then type “roxygen comment”)

  4. Run styler while in the doc/learning.qmd file with the Palette (Ctrl-Shift-P, then type “style file”).
  5. Then, add and commit the changes to the Git history, using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”).

Use the below code as a guide:

doc/learning.qmd
# Need to also add the Roxygen documentation 
import_saliva <- function(file_path) {
    # Paste the code to import saliva data you created 
    # from previous exercise
    saliva_data <- ___(
        ___
    )
    return(saliva_data)
}

# Test that the function works
___(here("data-raw/mmash/user_1/saliva.csv"))
Click for the solution. Only click if you are struggling or are out of time.
#' Import the MMASH saliva file.
#'
#' @param file_path Path to the user saliva data file.
#'
#' @return Outputs a data frame/tibble.
#'
import_saliva <- function(file_path) {
  saliva_data <- read_csv(
    file_path,
    col_select = -1,
    col_types = cols(
      samples = col_character(),
      cortisol_norm = col_double(),
      melatonin_norm = col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
  return(saliva_data)
}

# Test that this works
# import_saliva(here("data-raw/mmash/user_1/saliva.csv"))

6.5 Continuing the workflow

Really emphasize to cut and paste, so that the function in the doc/learning.qmd file is deleted and no longer kept in the Quarto document.

We’ve created two functions. Now we need to move those functions from the doc/learning.qmd file and into the R/ folder by cutting and pasting (not just copying). We do this for a few reasons:

  1. To prevent the Quarto document from becoming too long and having a large portion of R code over other text.
  2. To make it easier to maintain and find things in your project.
  3. To make use of the source() function.

We want to store our functions in the file R/functions.R script so its easier to source them. Cut and paste only the import_user_info() function we created in doc/learning.qmd, including the Roxygen documentation, and paste it into the newly created R/functions.R.

Once we have it in there, let’s test out the workflow. Restart our R session with by either going to the “Session -> Restart R” menu item or by using the keybindings Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-P, then type “restart”). Move back into the doc/learning.qmd and add source(here("R/functions.R")) to the code chunk called setup at the top. Run all the code inside the setup code chunk. Then go to where you wrote:

doc/learning.qmd
import_user_info(here::here("data-raw/mmash/user_1/user_info.csv"))

Now run this line. What happens now? You may get an error about not finding the read_csv() function. If you put library(tidyverse) in the setup code chunk, you might not get an error. If you did get an error, that’s because R doesn’t know what the read_csv() function is. This is where we start getting into package dependency management.

What is package dependency management? Whenever you use an R package, you depend on it for your code to work. The informal way to show what packages you use is by using the library() function. But if you come back to the project, or get a new computer, or someone else is working on your project too, how will they know which packages your project depends on? Do they have to search through all your files just to find all library() functions you used and then install those packages individually? A much better way here is to formally indicate your package dependency so that installing dependencies is easy. We do this by making use of the DESCRIPTION file.

Open up the DESCRIPTION file. You may or may not see something that looks like:

Type: Project
Package: LearnR3
Version: 0.0.1
Imports:
    knitr,
    rmarkdown
Encoding: UTF-8

If it doesn’t look like this, replace all of your current text with the text above. Notice the Imports: key. This is where information about packages are added. To quickly add a package, go to the Console and type out:

Console
usethis::use_package("readr")

You will see a bunch of text about adding it to Imports. If you look in your DESCRIPTION file now, you’ll see something like:

Type: Project
Package: LearnR3
Version: 0.0.1
Imports:
    knitr,
    readr,
    rmarkdown
Encoding: UTF-8

Now, if you or someone else wants to install all the packages your project depends on, they can do that by going to the Console and running:

Console
pak::pak()

This function finds the DESCRIPTION file and installs all the packages in Imports. Let’s add the other dependencies by typing in the Console:

Console
usethis::use_package("here")
usethis::use_package("fs")
usethis::use_package("snakecase")

Since we will also make use of the tidyverse set of packages later in the course, we’ll also add tidyverse as a dependency. Since the tidyverse is a large collection of packages, the recommended way to add this particular dependency is with:

Console
usethis::use_package("tidyverse", type = "Depends")

If you look in the DESCRIPTION file now, you see that the new Depends field has been added with tidyverse right below it. There are fairly technical reasons why we need to put tidyverse in the Depends field that you don’t need to know about for this course, aside from the fact that it is a common practice in R projects. At least in this context, we use the Depends field for tidyverse because of one big reason: the usethis::use_package() function will complain if we try to put tidyverse in the Imports and it recommends putting it in the Depends field.

Type: Project
Package: LearnR3
Version: 0.0.1
Depends:
    tidyverse
Imports:
    fs,
    here,
    knitr,
    readr,
    rmarkdown,
    snakecase
Encoding: UTF-8

Great! Now that we’ve formally established package dependencies in our project, we also need to formally declare which package each function comes from inside our own functions.

Reading task: ~5 minutes

Before getting into the correct way, we need to quickly cover the incorrect way that you may or may not have seen how others have done it on websites or in script files. Sometimes people use library() or require() inside functions like:

add_numbers <- function(num1, num2) {
    library(packagename)
    ...code...
    return(added)
}

Or:

add_numbers <- function(num1, num2) {
    require(packagename)
    ...code...
    return(added)
}

This is very bad practice and can have some unintended and serious consequences that you might not notice or that won’t give any warning or error. The correct way of indicating which package a function comes from is instead by using packagename::functionname, which you’ve seen and used many times in this course. We won’t get into the reasons why this is incorrect because it can quickly get quite technical.

You can also talk about why require() shouldn’t be used compared to library(). The problem with require() is that if the package can’t be loaded, it won’t throw an error, it only checks if the package is available and will otherwise continue running the code. On the other hand, library() will throw an error if it can’t find the package, which is what you expect if your code depends on a package.

Another reason to use packagename::functionname for each function from an R package you use in your own function is that it explicitly tells R (and us the readers) where the function comes from. Because the same function name can be used by multiple packages, if you don’t explicitly state which package the function is from, R will use the function that it finds first - which isn’t always the function you meant to use. We also do this step at the end of making the function because doing it while we create it can be quite tedious.

Tip

If you don’t know what package a function comes from when you need to append the package when using ::, you can find out what the package is by using the help documentation ?functionname (can also be done by pressing F1 when the cursor is over the function). The package name is at the very top left corner, surrounded by { }.

Alright, let’s go into R/functions.R and add readr:: to each of the readr functions we’ve used:

R/functions.R
import_user_info <- function(file_path) {
  info_data <- readr::read_csv(
    file_path,
    col_select = -1,
    col_types = readr::cols(
      gender = readr::col_character(),
      weight = readr::col_double(),
      height = readr::col_double(),
      age = readr::col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
  return(info_data)
}

Test that it works by restart the R session with Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-P, then type “restart”) and source the file with Ctrl-Shift-S or with the Palette (Ctrl-Shift-P, then type “source”), then go to the Console and type out:

Console
import_user_info(here::here("data-raw/mmash/user_1/user_info.csv"))

It should work as expected! Now that we’ve done that, let’s add and commit the changes made through the Git interface, using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”).

6.6 Exercise: Move and update the rest of the functions

Time: ~30 minutes.

Repeat this process of making functions by doing this to the rest of the code you worked on previously that imported the RR.csv and Actigraph.csv data.

  1. Convert the importing code into functions while in the doc/learning.qmd file. Include the Roxygen documentation and use packagename:: to be explicit about where the function comes from.

    • Name the new functions import_rr and import_actigraph.
  2. Move (by cutting and pasting) only the function into R/functions.R.

  3. Restart R, source() the functions file, using Ctrl-Shift-S or with the Palette (Ctrl-Shift-P, then type “source”), and test that the functions work by running them in the Console. The below code should run without a problem if you did it right:

    Console
    import_rr(here::here("data-raw/mmash/user_1/RR.csv"))
    import_actigraph(here::here("data-raw/mmash/user_1/Actigraph.csv"))
  4. Run styler while in the R/functions.R file with the Palette (Ctrl-Shift-P, then type “style file”).

Also update the import_saliva() function you created by being explicit about where the functions come from (e.g. with the packagename::). Then cut and paste this function along with its Roxygen documentation over into the R/functions.R file. Run styler with the Palette (Ctrl-Shift-P, then type “style file”) and afterwards, add and commit the changes to the Git history, using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”).

Use this code template as a guide for making the functions.

# Insert Roxygen documentation too
___ <- function(___) {
    ___ <- ___::___(
        ___,
        col_select = ___,
        col_types = ___::cols(
            ___
        ),
        name_repair = snakecase::to_snake_case
    )
    return(___)
}
Click for the solution. Only click if you are struggling or are out of time.
#' Import the MMASH saliva file
#'
#' @param file_path Path to the user saliva data file.
#'
#' @return Outputs a data frame/tibble.
#'
import_saliva <- function(file_path) {
  saliva_data <- readr::read_csv(
    file_path,
    col_select = -1,
    col_types = readr::cols(
      samples = readr::col_character(),
      cortisol_norm = readr::col_double(),
      melatonin_norm = readr::col_double()
    ),
    name_repair = snakecase::to_snake_case
  )
  return(saliva_data)
}

#' Import the MMASH RR file (heart beat-to-beat interval).
#'
#' @param file_path Path to the user RR data file.
#'
#' @return Outputs a data frame/tibble.
#'
import_rr <- function(file_path) {
  rr_data <- readr::read_csv(
    file_path,
    col_select = -1,
    col_types = readr::cols(
      ibi_s = readr::col_double(),
      day = readr::col_double(),
      # Converts to seconds
      time = readr::col_time(format = "")
    ),
    name_repair = snakecase::to_snake_case
  )
  return(rr_data)
}

#' Import the MMASH Actigraph file (accelerometer).
#'
#' @param file_path Path to the user Actigraph data file.
#'
#' @return Outputs a data frame/tibble.
#'
import_actigraph <- function(file_path) {
  actigraph_data <- readr::read_csv(
    file_path,
    col_select = -1,
    col_types = readr::cols(
      axis_1 = readr::col_double(),
      axis_2 = readr::col_double(),
      axis_3 = readr::col_double(),
      steps = readr::col_double(),
      hr = readr::col_double(),
      inclinometer_off = readr::col_double(),
      inclinometer_standing = readr::col_double(),
      inclinometer_sitting = readr::col_double(),
      inclinometer_lying = readr::col_double(),
      vector_magnitude = readr::col_double(),
      day = readr::col_double(),
      time = readr::col_time(format = "")
    ),
    name_repair = snakecase::to_snake_case
  )
  return(actigraph_data)
}

6.7 Summary

Quickly cover this before finishing the session and when starting the next session.

  • Functions in R are anything that does an action.
  • Functions have five components:
    • The three required ones are the function call with function() { }, the function body between the { }, and an output (usually set with return()).
    • The two optional ones are assigning the function to a named object with <- and the function arguments put within function().
  • Write function documentation by using Roxygen.
  • Use use_package() for the DESCRIPTION file as well as packagename::functionname() to explicit state the packages your function depends on.