nums <- numeric()
for (i in 1:20) {
if(i < 10) {
nums[i] <- i
}
}
print(nums)[1] 1 2 3 4 5 6 7 8 9
The functional programming paradigm borrows the mathematical idea of a function: a function will always return the same output if the same input is given. There is no way f(x) = y and f(x) = a. Functions that respect this rule are usually called pure functions. This makes it possible to treat functions as first class objects, that is to treat them the same way objects such as integers or strings are treated. In the example below, functions are given as arguments to the apply_stat function.
References:
In order to write pure functions, some good practices are recommended:
# Don't
bonus <- 45
adds_bonus <- function(salary){
bonus_salary <- salary + bonus
return(bonus_salary)
}
adds_bonus(100)[1] 145
# Do
adds_bonus_2 <- function(salary, bonus = 45){
bonus_salary <- salary + bonus
return(bonus_salary)
}
adds_bonus_2(100, 45)[1] 145
# Don't
melts_saves_df <- function(df){
melted_df <- df %>%
reshape2::melt()
write.csv(melted_df, "melted_df.csv")
}
melts_saves_df(mtcars)Error in `loadNamespace()`:
! there is no package called 'reshape2'
# Do
melts_df <- function(df) {
melted_df <- df %>%
reshape2::melt()
return(melted_df)
}
saves_melted_df <- function(df, file_name){
write.csv(df, file_name)
}
melted_df <- melts_df(mtcars)Error in `loadNamespace()`:
! there is no package called 'reshape2'
Error:
! object 'melted_df' not found
You can also apply both functions with the pipe:
References:
The purrr package provides functions that allow R code to be written in a simple and functional way. With purrr, we can:
The functions in purrr can be divided into a few families, where the map family is the most important one.
The map function takes as arguments a vector or list and a function. It will apply this function to the elements of the vector or list. The main point of map is to apply the same functions many times changing one or more parameters, “mapping” the function to these parameters.
In the example below, we use map to calculate the statistics of a given numeric vector.
Which is equivalent to the following, as the expression inside the map function is the same as what the apply_stat() does. This means that we can use inline functions inside map (which don’t exist in the environment), using the generic “.x” to indicate what is the function parameter:
stats_v2 <-
list(min, max, median, mean, sd) %>%
purrr::map(~.x(nums))
# min
stats[[1]]; stats_v2[[1]][1] 1
[1] 1
[1] 9
[1] 9
[1] 5
[1] 5
In order to easily access the results, we can use the get function from base R:
stats_names <- c("min", "max", "median", "mean", "sd")
named_stats <-
stats_names %>%
map(~get(.x)(nums)) %>%
set_names(stats_names)
names(named_stats)[1] "min" "max" "median" "mean" "sd"
[1] 1
[1] 5
Above, we had a list of functions to apply to same vector, but we can also have a list of vectors and apply the same function to all of them:
[[1]]
[1] 5.5
[[2]]
[1] -0.3765446
[[3]]
[1] 25
Thus, we can see that map takes vectors or lists and pass them as the first argument to a function: map(.x, .f). However, if the function .f accepts more than one argument, these arguments can be passed to .f: map(.x, .f, argument = value). In this case, argument = value will be passed to .f every time it called:
[[1]]
[1] NA
[[2]]
[1] NA
[[3]]
[1] NA
Now, setting na.rm = T
[[1]]
[1] 5.5
[[2]]
[1] -0.1667445
[[3]]
[1] 25
[[1]]
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
[[2]]
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21 6 160 110 3.9 2.62 16.46 0 1 4 4
There are several variations of the map function: map2, map_if, map_at. They all have the same principle: apply the same function to the element of a vector or list. map_if will apply the function to the elements of the list that satisfy a predicate function .p. map_at will apply the function to elements of the list at certain positions, the ones specified in the .at argument. Let’s look into map2 with more detail.
map2 was built to handle situations in which one needs to apply the same function to all the element of a list, but the function takes more than one argument and each element of the list takes a different value to this argument. Suppose you have a list of data frames and you want to save them in your directory with different names. In the example below, .x will be passed as the first argument to write.csv and .y will be passed as the second argument to write.csv
Ex 1:
dfs <- list(
as.data.table(mtcars,keep.rownames = TRUE),
data.frame(AirPassengers)
)
file_names <- c("mtcars_df.csv","air_passengers_df.csv")
map2(.x = dfs, .y = file_names, write.csv)[[1]]
NULL
[[2]]
NULL
Note that the row names were saved to both csv files. We can set the argument row.names = FALSE to both calls of the function like this:
file_names <- c("mtcars_df_sem_rn.csv","air_passengers_df_sem_rn.csv")
map2(.x = dfs, .y = file_names, write.csv, row.names = FALSE)[[1]]
NULL
[[2]]
NULL
Ex 2:
[[1]]
[1] 2
[[2]]
[1] 12
[[3]]
[1] 103
If we want to map more than two arguments, we can use pmap(), the generalization for a p number of parameters:
And, where the names of the list (e.g. x, y, z) will already be ‘matched’ by the function parameter names:
The reduce family functions are built to “reduce a list to a single value by iteratively applying a binary function”, according to its help page.
Ex 1:
[1] "G" "H" "I" "J"
Ex 2:
dfs_list <- list(
minusculas = data.table(id = 1:10, minusculas = letters[1:10]),
maiusculas = data.table(id = 1:10, maiusculas = LETTERS[1:10]),
nomes = data.table(
id = 1:5,
nomes = c("Bruna","Igor","Milton","Rodrigo","Victor"))
)
reduce(dfs_list, merge, by = "id")Key: <id>
id minusculas maiusculas nomes
<int> <char> <char> <char>
1: 1 a A Bruna
2: 2 b B Igor
3: 3 c C Milton
4: 4 d D Rodrigo
5: 5 e E Victor
Like in map, we can set the values to the other argument of the function .f
These functions are used to filter vectors:
# Let's use this space to introduce and explain the pluck family. pluck() is a
# function used to pull elements of a list. pluck(.x, ...), in which .x is a list
# and ... accepts a numeric, indicating the position of the element you want to pull,
# or a string, indicating the name of the element you want to pull
# Extracting the names that contain the letter "R". Note that stringr::str_detect
# is case sensitive.
dfs_list %>%
purrr::pluck("nomes") %>%
dplyr::pull(nomes) %>%
purrr::keep(stringr::str_detect, "R|r")[1] "Bruna" "Igor" "Rodrigo" "Victor"
or:
The compose family is used to create composed functions, just like in mathematics.
lista <- list(
c(rep("A",4)),
c("a",letters),
dfs_list$nomes$nomes
)
lista %>%
map(compose(length, unique))[[1]]
[1] 1
[[2]]
[1] 26
[[3]]
[1] 5
which is equivalent to:
So far we’ve looked into functions that allow us to apply functions into elements of lists. However it is also useful sometimes to alter the structure of a list. Let’s take a look into these functions:
flatten is used to trasnform a deep list (list of lists) into a single list:
transpose is used to invert the structure of the list: