#> function (x, na.rm = FALSE)
#> sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x),
#> na.rm = na.rm))
#> <bytecode: 0x55de28026ac8>
#> <environment: namespace:stats>
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.
6.1 Learning objectives
The overall learning outcome for this session is to:
- Describe a basic workflow for creating functions and then apply this workflow to import data.
Specific objectives are to:
- 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.
- Describe a workflow for developing code that combines using Quarto,
source()
’ing with Ctrl-Shift-SCtrl-Shift-S or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “source”), and restarting R with Ctrl-Shift-F10Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “restart”), explain its usefulness in assessing correctness and re-usability of code, and then apply this workflow when developing functions. - 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. - 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 usingsource()
to load the functions into the R session. - Continue practicing Git version control to manage changes to your files.
6.2 The basics of a function
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-ICtrl-Alt-I or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “new chunk”) . Then, inside the code chunk, we’ll write this code out:
You can use the new function by running the above code and writing out your new function, with arguments to give it.
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-RCtrl-Shift-Alt-R or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “roxygen comment”), and you can add template documentation right above the function. It looks like:
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:
Once we’ve created that, let’s open up the Git Interface with Ctrl-Alt-MCtrl-Alt-M or with the Palette (Ctrl-Shift-PCtrl-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:
- Write code that works and does what you want.
- Enclose it as a function with
name <- function() { ... }
, with an appropriate and descriptive name. - Create arguments in the function call (
function(argument1, argument2)
) with appropriate and descriptive names, then replace the code with the argument names where appropriate. - Rename any objects created to be more generic and include the
return()
function at the end to indicate what the function will output. - Run the function and check that it works.
- Add the Roxygen documentation tags with Ctrl-Shift-Alt-RCtrl-Shift-Alt-R or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “roxygen comment”) while the cursor is in the function.
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-ICtrl-Alt-I or with the Palette (Ctrl-Shift-PCtrl-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:
Next we wrap it in the function call and give it an appropriate name. In this case, import_user_info
is descriptive and meaningful.
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
.
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.
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.
#> # A tibble: 1 × 4
#> gender weight height age
#> <chr> <dbl> <dbl> <dbl>
#> 1 M 65 169 29
#> # 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-PCtrl-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-MCtrl-Alt-M or with the Palette (Ctrl-Shift-PCtrl-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
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-OCtrl-Shift-O or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “outline”).
- Create a new markdown header
## Exercise for importing the saliva data as a function
. - Create a new code chunk below the header with Ctrl-Alt-ICtrl-Alt-I or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “new chunk”).
- 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 replaceuser_1_saliva_file
withfile_path
in theread_csv()
codeRename the output object to
saliva_data
and put it in thereturn()
functionRun the function and then test that it works
-
Create the Roxygen documentation by adding it with
Ctrl-Shift-Alt-RCtrl-Shift-Alt-R or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “roxygen comment”)
- Run styler while in the
doc/learning.qmd
file with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “style file”). - Then, add and commit the changes to the Git history, using Ctrl-Alt-MCtrl-Alt-M or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “commit”).
Use the below code as a guide:
doc/learning.qmd
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
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:
- To prevent the Quarto document from becoming too long and having a large portion of R code over other text.
- To make it easier to maintain and find things in your project.
- 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-F10Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-PCtrl-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:
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:
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:
This function finds the DESCRIPTION
file and installs all the packages in Imports
. Let’s add the other dependencies by typing in the Console:
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:
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.
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-F10Ctrl-Shift-F10 or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “restart”) and source the file with Ctrl-Shift-SCtrl-Shift-S or with the Palette (Ctrl-Shift-PCtrl-Shift-P, then type “source”), then go to the Console and type out:
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-MCtrl-Alt-M or with the Palette (Ctrl-Shift-PCtrl-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.
-
Convert the importing code into functions while in the
doc/learning.qmd
file. Include the Roxygen documentation and usepackagename::
to be explicit about where the function comes from.- Name the new functions
import_rr
andimport_actigraph
.
- Name the new functions
Move (by cutting and pasting) only the function into
R/functions.R
.-
Restart R,
source()
the functions file, using Ctrl-Shift-SCtrl-Shift-S or with the Palette (Ctrl-Shift-PCtrl-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: Run styler while in the
R/functions.R
file with the Palette (Ctrl-Shift-PCtrl-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-PCtrl-Shift-P, then type “style file”) and afterwards, add and commit the changes to the Git history, using Ctrl-Alt-MCtrl-Alt-M or with the Palette (Ctrl-Shift-PCtrl-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
- Functions in R are anything that does an action.
- Functions have five components:
- Write function documentation by using Roxygen.
- Use
use_package()
for theDESCRIPTION
file as well aspackagename::functionname()
to explicit state the packages your function depends on.