I use this space to practice functional programming techniques in R.
These examples are derived from a problem described in Functional Thinking by Neal Ford (O’Reilly), Copyright 2014 Neal Ford, 978-1-449-36551-6. It’s a very insightful book, I recommend you check it out.
# Functional Thinking by Neal Ford, p.13
employees <- c("neal", "s", "stu", "j", "rich", "bob", "aiden", "j",
"ethan", "liam", "mason", "noah", "lucas", "jacob",
"jayden", "jack")
employees
## [1] "neal" "s" "stu" "j" "rich" "bob" "aiden"
## [8] "j" "ethan" "liam" "mason" "noah" "lucas" "jacob"
## [15] "jayden" "jack"
The mission here is to return a string of all names greater than one character separated by a comma.
First, an imperative example
x <- list()
for(i in 1:length(employees)) {
if(nchar(employees[i]) > 1) {
x[[i]] <- employees[i]
x[[i]] <- paste0(toupper(substr(x[[i]], 1, 1)),
tolower(substr(x[[i]], 2, nchar(x[[i]]))))
}
}
employees <- unlist(x)
employees
## [1] "Neal" "Stu" "Rich" "Bob" "Aiden" "Ethan" "Liam"
## [8] "Mason" "Noah" "Lucas" "Jacob" "Jayden" "Jack"
Now, a functional version. Notice there is no implied concept of ordering unlike the imperative example (for(i in 1:length(employees))
).
library(magrittr) # supplies the conveient %>% operator
##
## Attaching package: 'magrittr'
## The following object is masked from 'package:purrr':
##
## set_names
employees %>%
Filter(function(x) nchar(x) > 1, .) %>%
# notice output is a list, not a vector
Map(function(x) { paste0(toupper(substr(x, 1, 1)),
tolower(substr(x, 2, nchar(x)))) }, .) %>%
as.character()
## [1] "Neal" "Stu" "Rich" "Bob" "Aiden" "Ethan" "Liam"
## [8] "Mason" "Noah" "Lucas" "Jacob" "Jayden" "Jack"
Employees <- employees %>%
Filter(function(x) nchar(x) > 1, .) %>%
# notice output is a list, not a vector
Map(function(x) { paste0(toupper(substr(x, 1, 1)),
tolower(substr(x, 2, nchar(x)))) }, .)
Multi-arity functions (varadic functions) contain built-in reducers. Like paste
in R.
Employees %>%
paste(., collapse = ", ")
## [1] "Neal, Stu, Rich, Bob, Aiden, Ethan, Liam, Mason, Noah, Lucas, Jacob, Jayden, Jack"
Employees %>%
Reduce(function(x, y) { paste0(x, ", ", y) }, .)
## [1] "Neal, Stu, Rich, Bob, Aiden, Ethan, Liam, Mason, Noah, Lucas, Jacob, Jayden, Jack"
suppressPackageStartupMessages(library('purrr'))
employees %>%
keep(~ nchar(.x) > 1) %>%
map(~ paste0(toupper(substr(.x, 1, 1)), tolower(substr(.x, 2, nchar(.x))))) %>%
paste(collapse = ", ")
## [1] "Neal, Stu, Rich, Bob, Aiden, Ethan, Liam, Mason, Noah, Lucas, Jacob, Jayden, Jack"
capitalize <- function(x) {
paste0(toupper(substr(x, 1, 1)), tolower(substr(x, 2, nchar(x))))
}
employees %>%
keep(~ nchar(.x) > 1) %>%
map(~ capitalize(.x)) %>%
unlist
## [1] "Neal" "Stu" "Rich" "Bob" "Aiden" "Ethan" "Liam"
## [8] "Mason" "Noah" "Lucas" "Jacob" "Jayden" "Jack"
Compose is interesting, it takes two functions and returns a new function that applies both given functions. This lets you build up compositions of functions.
exclaim <- function(x) { paste0(x, "!") }
vip <- compose(capitalize, exclaim)
c("jamal", "andrea") %>%
vip()
## [1] "Jamal!" "Andrea!"
Closures are useful for maintaining state across invocation.
counter <- function() {
i <- 0
function() {
# notice the <<- operator pushes the result up one
# parent environment.
(i <<- i + 1)
}
}
a_counter <- counter()
a_counter()
## [1] 1
a_counter()
## [1] 2
a_counter()
## [1] 3
A new invocation uses the initial state. In this case i <- 0
.
a_new_counter <- counter()
a_new_counter()
## [1] 1
a_new_counter()
## [1] 2
Now the imperative version for comparison.
counter <- 0
counter <- counter + 1
counter
## [1] 1
counter <- counter + 1
counter
## [1] 2
With the imperative version you’ve got to keep track of the state of the counter
variable. Modifying counter
accidentally is an easy mistake to make.
raise <- function(x, y) x^y
square <- function(x) raise(x, 2)
cube <- function(x) raise(x, 3)
square(2)
## [1] 4
cube(2)
## [1] 8
Currying also enbables composition. Composition relies on having two functions that take a single argument.
raise_to_6th <- compose(square, cube)
raise_to_6th(2)
## [1] 64
Currying seems useful in that it allows you to pre-fill arguments. I commonly find myself overriding default boolean flags in functions like append = TRUE
or stringsAsFactors = FALSE
.
For example,
data.frame(id = 1:3, names = c("Inez", "Drake", "Linux")) %>%
str()
## 'data.frame': 3 obs. of 2 variables:
## $ id : int 1 2 3
## $ names: Factor w/ 3 levels "Drake","Inez",..: 2 1 3
data.frame2 <- function(...) {
data.frame(..., stringsAsFactors = FALSE)
}
data.frame2(id = 1:3, names = c("Inez", "Drake", "Linux")) %>%
str()
## 'data.frame': 3 obs. of 2 variables:
## $ id : int 1 2 3
## $ names: chr "Inez" "Drake" "Linux"
Currying and it’s cousin “partial application” can allow you to fill some arguments ahead of time. This can reduce the amount of code you have to write or repeat. Bugs are highly correlated with lines of code, so reducing the quantity of code will tend to reduce bugs.