3 Programming Structures

3.1 Three basic control flows

3.1.1 Sequence

Code runs line by line in sequence with no conditional executions or loops.

Let’s see an example: calculating present value (PV) of a cash flow. Suppose you can get $1M bonus at year end for the next three years, and the yields on 1-. 2-, and 3-year Treasury securities are (2%, 3%, 2%). To calculate the PV of this fortune, we can use the following formula.

\[PV = \sum_{t=1}^{3} \frac{1}{(1 + r_t)^t},\]

where \(r_t\) is the yield on t-year Treasury securities.

# yield curve rate for the next 3 years
rates <- c(0.02, 0.03, 0.02)

# time
t <- seq(1, 3)

# calculating the present value 
pv <- sum(1 / (1 + rates)^t)

pv
## [1] 2.86531

3.1.2 Selection / Conditional execution

# if and else template; don't run this cell
if (condition) {
  # code executed when condition is TRUE
} else {
  # code executed when condition is FALSE
}
# if, else if, else template; don't run this cell
if (condition1) {
  # do something when condition1 is TRUE
} else if (conditon2) {
  # do something else if condition1 is FALSE but condition2 is TRUE
} else {
  # do something if neither condition1 nor condition2 is TRUE
}

For example, based on the above PV calculation, you need to decide between two choices: 1) take 2.8M dollar today; 2) get 1M at the end of each next three years. Which one will you choose?

# yield curve rate for the next 3 years
rates <- c(0.02, 0.03, 0.02)

# time
t <- seq(1, 3)

# calculating the present value 
pv <- sum(1 / (1 + rates)^t)

if (pv < 2.8) {
  print("take choice 1")
} else {
  print("take choice 2")
}
## [1] "take choice 2"

There is also a ifelse(test, yes, no) function that can sometimes be handy.

cond = FALSE
t_f <- ifelse(cond, "T", "F")
t_f
## [1] "F"
# ifelse() works with vectors too
n_vec <- c(2, 3, 4)

# %% is modulus operator (Remainder from division)
ifelse(n_vec %% 2 == 0, "even", "odd")
## [1] "even" "odd"  "even"

Exercise

Reproduce the PV choice example above using the ifelse() function.

# your code here
decision <- ifelse(pv < 2.8, "take choice 1", "take choice 2")
print(decision)
## [1] "take choice 2"

3.1.3 Iteration

# for loop template; don't run this cell
for (var in seq) {
  do something
}
# while loop template; don't run this cell
while (condition) {
  do something if condition is TRUE
}

For example, you can solve the PV by using iteration. In general, though, you should always avoid loops if you can (such as in this case). We will discuss how to code less loops later.

# yield curve rate for the next 3 years
rates <- c(0.02, 0.03, 0.02)

pv <- 0

# for loop over index of rates
for (t in seq_along(rates)) {
  # print(t)
  pv <- pv + 1 / (1 + rates[t]) ^ t
}
print(pv)
## [1] 2.86531
# you can also use the for loop over element of rates
rates <- c(0.02, 0.03, 0.02)
pv <- 0
t <- 1
for (rate in rates) {
  pv <- pv + 1 / (1 + rate) ^ t
  t <- t + 1
}
print(pv)
## [1] 2.86531

Exercise

Calculate the PV using a while loop.

# your code here
# you could use while loop as well
rates <- c(0.02, 0.03, 0.02)
pv <- 0
t <- 1
while (t <= 3) {
  pv <- pv + 1 / (1 + rates[t]) ^ t
  t <- t + 1
}
print(pv)
## [1] 2.86531

3.2 Functions

A function is a unit of code block that (usually) takes in some inputs (arguments) and returns a result. We have already used many R functions, for example, print, seq, and seq_along. The two main reasons to write functions are reusability and abstraction. If you find yourself repeating the the same code logic or if you want to divide a large piece of code into small logical units, you should consider writing functions.

In fact, “everything that happens [in R] is a function call.” (John Chambers, the creator of the S programming language and a core member of the R project. R is modelled after S.) For example, even the plus operation + is a function.

Let’s continue our PV example. Let’s write a slightly general version of PV calculating code by implementing a function calcuate_pv. This function takes in two argument rates and cash_flow (with arbitrary number of years), and return the present value calculated.

# make the pv calucation a function
calculate_pv <- function (rates, cash_flow) {
  t <- 1:length(rates)
  return(sum(cash_flow / (1 + rates)^t))
}

# test 1
cash_flow1 <- c(1, 1, 1)
rates1 <- c(0.02, 0.03, 0.02)
print(calculate_pv(rates1, cash_flow1))
## [1] 2.86531
# test 2
cash_flow2 <- c(1, 2, 3, 4, 5)
rates2 <- c(0.02, 0.03, 0.02, 0.03, 0.04)
print(calculate_pv(rates2, cash_flow2))
## [1] 13.35613

What will happen if your cash flow and rates has different length?

# test it yourself
cash_flow3 <- c(1, 1, 1)
rates3 <- c(0.02, 0.03)
print(calculate_pv(rates3, cash_flow3))
## Warning in cash_flow/(1 + rates)^t: longer object length is not a multiple
## of shorter object length
## [1] 2.90338

It still gives you a result, but why? (Hint: recycling rule)

# an example of recycling
print(c(1, 2, 3, 4) / c(1, 2, 3, 4))
## [1] 1 1 1 1
print(c(1, 2, 3, 4) / c(1, 2))
## [1] 1 1 3 2

In light of the above hard-to-detect bug, it’s a good practice to do some basic sanity checks for the arguments before using them (don’t overdo it though).

Exercise

Modify the above PV-calculating function so that when rates and cash_flow are of different length, the function stops and display an error message.

Hint: The function stop("msg") stops R code and displays an error message “msg”.

# your code here
# a function calculating pv of a cash flow for a given rates
calculate_pv <- function(rates, cash_flow) {
  
  # check rates and cash_flow has the same length
  if (length(rates) != length(cash_flow)) {
    stop("Arguments input error! rates and cash_flow must have the same length")
  }
  
  t <- 1:length(rates)
  sum(cash_flow / (1 + rates)^t)
  return(pv)
  
}

cash_flow3 <- c(1, 1, 1)
rates3 <- c(0.02, 0.03)
print(calculate_pv(rates3, cash_flow3))
## Error in calculate_pv(rates3, cash_flow3): Arguments input error! rates and cash_flow must have the same length

The functions we wrote so far explicitly return a value using return(). If there is no return() in a function, the value of the last evaluated expression is returned automatically. In addition, return() can only return a single object. To return multiple values from a function, use a vector or list.

The next function finds the maximum and minimum values in a vector.

# a function to find the max and min elements in a numeric vector
find_max_min <- function(vec) {
  
  if (!is.numeric(vec)) {
    stop("Input must be numeric vector (integer or double)!")
  }
  
  c(max(vec), min(vec))
}

# testing
sp_index_return1 <- c(0.02, 0.03, 0.02)
print(find_max_min(sp_index_return1))
## [1] 0.03 0.02
sp_index_return2 <- c(0.02, 0.03, 0.02, 0.03, 0.04)
print(find_max_min(sp_index_return2))
## [1] 0.04 0.02
# a vector with missing number: NA
sp_index_return3 <- c(0.02, 0.03, 0.02, 0.03, NA)
print(find_max_min(sp_index_return3))
## [1] NA NA
# test a non-numeric vector
print(find_max_min(c("a", "b", "c")))
## Error in find_max_min(c("a", "b", "c")): Input must be numeric vector (integer or double)!

You can specify default values for arguments in a function.

find_max_min <- function (vec, na_remove = TRUE) {
  
  if (!is.numeric(vec)) {
    stop("Input must be numeric vector (integer or double)!")
  }
  
  if (na_remove) {
    c(max(vec, na.rm = TRUE), min(vec, na.rm = TRUE))
  } else {
    c(max(vec), min(vec))
  }
}

sp_index_return3 <- c(0.02, 0.03, 0.02, 0.03, NA)
print(find_max_min(sp_index_return3))
## [1] 0.03 0.02
print(find_max_min(sp_index_return3, na_remove = FALSE))
## [1] NA NA

There are many details about functions in R. We will briefly discuss one of them below: the concept of scoping.

3.2.1 Scoping

Scoping is about finding the value associated with a name (think of “name” as a variable name for now). The basic rule is that names defined inside a function mask names defined outside a function. If a name used inside a function is not defined inside the function, R looks one level up.

Without running the following code, can you predict what the output will be?

x <- 10
y <- 20
f1 <- function() {
  x <- 20
  c(x, y)
}

print(f1())
## [1] 20 20
print(x)
## [1] 10
# won't shown in student version

# extra 1
# <<-

# extra 2
x <- 10

`f2<-` <- function(x, value) {
  x <- value
  x
}

f2(x) <- 20
print(x)
## [1] 20

Functions in R are objects like vectors and lists. Just as a function can take in vectors as arguments and return vectors as output, it can also take in functions as arguments and return functions. This feature of R is very powerful. It allows you to program in a functional style that can often produce efficient and elegant code. We’ll briefly discuss one concept related to this coding style (the rest will be optional materials for you to study yourself). Some very useful R packages use the functional idea, so you should at least be comfortable with it.

3.2.2 Functional

A functional takes in a function (or functions) as input and returns a vector/list. Below is a simple example.

# perform an operation (f) on outcomes of two 6-face dices, and return the result
two_dices_op <- function(f) {
  # sample(x, size, replace = FALSE, prob = NULL)
  x <- sample(1:6, 2, replace = TRUE)
  f(x)
}

two_dices_op(sum)
## [1] 8
# two_dices_op(prod)
# two_dices_op(print)

Functional is easy to understand and very commonly used. For example, the optim function in base R takes in a (mathematical) function you want to optimize on.

# define and plot a function
y <- function (x) (x - 1)^2
plot(y, -1, 3, n = 50, main = "y = (x - 1)^2")

# find the minimum
result <- optim(c(0), y, method = "BFGS")
str(result)
## List of 5
##  $ par        : num 1
##  $ value      : num 5.97e-30
##  $ counts     : Named int [1:2] 9 3
##   ..- attr(*, "names")= chr [1:2] "function" "gradient"
##  $ convergence: int 0
##  $ message    : NULL

purrr is a R package that provides many useful functionals. Let’s see an example. Suppose we want to find the maximum and minimum values for each column variables in the mtcars dataset. (mtcars is a built-in dataset in R.)

mtcars
##                      mpg cyl  disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4           21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag       21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710          22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive      21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout   18.7   8 360.0 175 3.15 3.440 17.02  0  0    3    2
## Valiant             18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1
## Duster 360          14.3   8 360.0 245 3.21 3.570 15.84  0  0    3    4
## Merc 240D           24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
## Merc 230            22.8   4 140.8  95 3.92 3.150 22.90  1  0    4    2
## Merc 280            19.2   6 167.6 123 3.92 3.440 18.30  1  0    4    4
## Merc 280C           17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4
## Merc 450SE          16.4   8 275.8 180 3.07 4.070 17.40  0  0    3    3
## Merc 450SL          17.3   8 275.8 180 3.07 3.730 17.60  0  0    3    3
## Merc 450SLC         15.2   8 275.8 180 3.07 3.780 18.00  0  0    3    3
## Cadillac Fleetwood  10.4   8 472.0 205 2.93 5.250 17.98  0  0    3    4
## Lincoln Continental 10.4   8 460.0 215 3.00 5.424 17.82  0  0    3    4
## Chrysler Imperial   14.7   8 440.0 230 3.23 5.345 17.42  0  0    3    4
## Fiat 128            32.4   4  78.7  66 4.08 2.200 19.47  1  1    4    1
## Honda Civic         30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
## Toyota Corolla      33.9   4  71.1  65 4.22 1.835 19.90  1  1    4    1
## Toyota Corona       21.5   4 120.1  97 3.70 2.465 20.01  1  0    3    1
## Dodge Challenger    15.5   8 318.0 150 2.76 3.520 16.87  0  0    3    2
## AMC Javelin         15.2   8 304.0 150 3.15 3.435 17.30  0  0    3    2
## Camaro Z28          13.3   8 350.0 245 3.73 3.840 15.41  0  0    3    4
## Pontiac Firebird    19.2   8 400.0 175 3.08 3.845 17.05  0  0    3    2
## Fiat X1-9           27.3   4  79.0  66 4.08 1.935 18.90  1  1    4    1
## Porsche 914-2       26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
## Lotus Europa        30.4   4  95.1 113 3.77 1.513 16.90  1  1    5    2
## Ford Pantera L      15.8   8 351.0 264 4.22 3.170 14.50  0  1    5    4
## Ferrari Dino        19.7   6 145.0 175 3.62 2.770 15.50  0  1    5    6
## Maserati Bora       15.0   8 301.0 335 3.54 3.570 14.60  0  1    5    8
## Volvo 142E          21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2
str(mtcars)
## 'data.frame':    32 obs. of  11 variables:
##  $ mpg : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
##  $ cyl : num  6 6 4 6 8 6 8 4 4 6 ...
##  $ disp: num  160 160 108 258 360 ...
##  $ hp  : num  110 110 93 110 175 105 245 62 95 123 ...
##  $ drat: num  3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
##  $ wt  : num  2.62 2.88 2.32 3.21 3.44 ...
##  $ qsec: num  16.5 17 18.6 19.4 17 ...
##  $ vs  : num  0 0 1 1 0 1 0 1 1 1 ...
##  $ am  : num  1 1 1 0 0 0 0 0 0 0 ...
##  $ gear: num  4 4 4 3 3 3 3 4 4 4 ...
##  $ carb: num  4 4 1 1 2 1 4 2 2 4 ...

Note that mtcars is a dataframe, a special (named) list of vectors (columns), so we can easily loop through the columns using a for loop. However, using the functional map from the purrr package makes the code more concise.

# using loop
for (i in seq_along(mtcars)) {
  print(find_max_min(mtcars[[i]]))
}
## [1] 33.9 10.4
## [1] 8 4
## [1] 472.0  71.1
## [1] 335  52
## [1] 4.93 2.76
## [1] 5.424 1.513
## [1] 22.9 14.5
## [1] 1 0
## [1] 1 0
## [1] 5 3
## [1] 8 1
# using map from purrr pacakge
library(purrr)
map(mtcars, find_max_min)
## $mpg
## [1] 33.9 10.4
## 
## $cyl
## [1] 8 4
## 
## $disp
## [1] 472.0  71.1
## 
## $hp
## [1] 335  52
## 
## $drat
## [1] 4.93 2.76
## 
## $wt
## [1] 5.424 1.513
## 
## $qsec
## [1] 22.9 14.5
## 
## $vs
## [1] 1 0
## 
## $am
## [1] 1 0
## 
## $gear
## [1] 5 3
## 
## $carb
## [1] 8 1

map(x, f) applies the function f to each element of a list x and returns a list. In our case, mtcars is a data frame, but it’s also a named list, i.e. a list of columns. find_max_min is applied to each column, and the result is returned as list.

In our example, the find_max_min() function is pre-defined. Sometimes it’s convenient to define a function on the spot (e.g. when calling the map() function above). You can do so using anonymous functions. An anonymous function is a function declared without a name.

# a simple example of anonymous function
(function(x, y) x + y)(2, 3)
## [1] 5
# anonymous function in the map
map(c(1, 2, 3), function(x) {x * x})
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 4
## 
## [[3]]
## [1] 9

Exercise

Using the map() function in purrr package and an anonymous function, find the maximum and minimum values for each column variables in the mtcars dataset.

# your code here
# using map from purrr pacakge & anonymous function
str(map(mtcars, function(x) c(max(x), min(x))))
## List of 11
##  $ mpg : num [1:2] 33.9 10.4
##  $ cyl : num [1:2] 8 4
##  $ disp: num [1:2] 472 71.1
##  $ hp  : num [1:2] 335 52
##  $ drat: num [1:2] 4.93 2.76
##  $ wt  : num [1:2] 5.42 1.51
##  $ qsec: num [1:2] 22.9 14.5
##  $ vs  : num [1:2] 1 0
##  $ am  : num [1:2] 1 0
##  $ gear: num [1:2] 5 3
##  $ carb: num [1:2] 8 1

purrr has it’s own short-cut of defining an anonymous function. See below for an example. However, let’s not worry about it for now.

# using map from purrr pacakge & anonymous function
# define the anonymous function the purrr way
str(map(mtcars, ~ c(max(.x), min(.x))))
## List of 11
##  $ mpg : num [1:2] 33.9 10.4
##  $ cyl : num [1:2] 8 4
##  $ disp: num [1:2] 472 71.1
##  $ hp  : num [1:2] 335 52
##  $ drat: num [1:2] 4.93 2.76
##  $ wt  : num [1:2] 5.42 1.51
##  $ qsec: num [1:2] 22.9 14.5
##  $ vs  : num [1:2] 1 0
##  $ am  : num [1:2] 1 0
##  $ gear: num [1:2] 5 3
##  $ carb: num [1:2] 8 1

map() can help avoiding loops in many ways. For example, let’s say you want to create a list of 3 vectors. Each vector contains 5 random numbers drawn from uniform [0, 1]. Instead of using a loop, you can do the following.

# you can also use the purrr shortcut way, xs <- map(1:3, ~ runif(5))
xs <- map(1:3, function(x) runif(5))
xs
## [[1]]
## [1] 0.9487490 0.2166964 0.3817401 0.6846513 0.5103878
## 
## [[2]]
## [1] 0.06595557 0.09910851 0.15397180 0.88460767 0.57018432
## 
## [[3]]
## [1] 0.8512746 0.1686550 0.9993348 0.7146104 0.6294237

Exercise

Write a functional to calculate \[\sum_{x=a}^{b} f(x),\] where \(a < b\) are integers and \(f()\) is a general function that you can define later (e.g. \(f(x) = x ^ 2\) or \(f(x) = x ^ 3\)).

# your code here
f_sum <- function(f, a, b) {
  if (a > b || !is.integer(a) || !is.integer(a)) {
    stop("argument input error!")
  }
  
  result = 0
  
  while (a <= b) {
    result = result + f(a)
    a = a + 1L
  }
  
  result
}

# define a sum_squared function using f_sum(f, a, b)
sum_squared <- function(a, b) {
  f_sum(function(x) {x ^ 2}, a, b)
}

# define a sum_cubed function using f_sum(f, a, b)
sum_cubed <- function(a, b) {
  f_sum(function(x) {x ^ 3}, a, b)
}

print(sum_squared(1L, 3L))
## [1] 14
print(sum_cubed(1L, 3L))
## [1] 36

You can of course use f_sum with anonymous functions.

# use f_sum with anonymous function
print(f_sum(function(x) {x ^ 2}, 1L, 3L))
## [1] 14
# calcuate pi using the pi formula
f_pi <- function(x) {
  4 * (-1) ^ (x + 1) / (2 * x - 1)
}

print(f_sum(f_pi, 1L, 100L))
## [1] 3.131593
library(purrr)
f_sum2 <- function(f, a, b) {
  if (a > b || !is.integer(a) || !is.integer(a)) {
    stop("argument input error!")
  }
  
  sum(unlist(map(a:b, f)))

}

print(f_sum2(function(x) {x ^ 2}, 1L, 3L))
## [1] 14
f_pi <- function(x) {
  4 * (-1) ^ (x + 1) / (2 * x - 1)
}

print(f_sum2(f_pi, 1L, 10000L))
## [1] 3.141493
A Note on the exercise (optional)

There is one approach to the above exercise that you perhaps haven’t thought about. That is the recursive approach. It uses a recursive function, a function that calls itself.

# simple recursive version
f_sum3 <- function(f, a, b) {
  if (a > b || !is.integer(a) || !is.integer(a)) {
    stop("argument input error!")
  }
  
  if (a == b) f(a) else f(a) + f_sum3(f, a + 1L, b)
  
}

# f is x^2
print(f_sum3(function(x) {x ^ 2}, 1L, 3L))
## [1] 14
# calculating pi
f_pi <- function(x) {
  4 * (-1) ^ (x + 1) / (2 * x - 1)
}

print(f_sum3(f_pi, 1L, 1000L))
## [1] 3.140593

You may not be comfortable with the recursive thinking yet. For the above problem, a loop is perfectly fine. However, for certain problems, it’s extremely helpful to write/think recursively.

The above recursive approach has a problem though. What will happen if you run f_sum3(f_pi, 1L, 10000L).

f_sum3(f_pi, 1L, 10000L)

The error you got is stack overflow. f_sum3(f_pi, 1L, 10000L) cannot return until f_sum3(f_pi, 2L, 10000L) returns, and so on. R keeps track of all of the function calls in a data structure called call stack. The stack contains references to all outstanding calls and is stored in the memory. When there are too many functions to track, the stack overflows.

One way to solve the problem is to write the recursive code in a tail recursion way. This is a kind of recursion that can easily be transformed into a loop (i.e., you can still think recursively, but you don’t need to worry about stack overflow). R doesn’t support auto-transformation between tail recursion and loop (some languages do such as Javascript, Scala, etc.), so you would need to build a simple transformation yourself. If you are interested in how it’s done, see this excellent blog post here.

3.2.3 Function factories (optional)

A function factory is a function that returns a function.

power1 <- function(exp) {
  force(exp)
  
  function(x) {
    x ^ exp
  }
}

square <- power1(2)
cube <- power1(3)

print(square(2))
## [1] 4
print(cube(2))
## [1] 8

force() is used to force evaluate the expression exp. Functions in R evaluate their arguments ‘lazily’, i.e., arguments are evaluated only if accessed. Without using force evaluation here, confusing behaviour will arise.

You can read more about lazy evaluation here.

# not using force evaluation
power2 <- function(exp) {
  function(x) {
    x ^ exp
  }
}

exp2 <- 2
square2 <- power2(exp2)
exp2 <- 3

square2(2)
## [1] 8

Let’s try another example. Let’s re-do the last exercise, the one about \(_{x=a}^{b} f(x)\), in a slightly different way.

f_sum_new <- function(f) {
  
  # return a function
  function(a, b) {
    
    result = 0
    while (a <= b) {
      result = result + f(a)
      a = a + 1L
    }
    result
  }
}

sum_squares_new <- f_sum_new(function(x) {x ^ 2})
sum_cubes_new <- f_sum_new(function(x) {x ^ 3})

print(sum_squares_new(1, 3))
## [1] 14
print(sum_cubes_new(1, 3))
## [1] 36

In the above example, the function f_sum_new(f) takes in a function (f) and returns a function too. The returned function, which itself takes in arguments a and b, applies the given function input (f) to integers from a to b, and sums the results.

Note that the f_sum_new(f) function takes in a function and returns a function. Some people call this kind of function as an function operator.

# the same as the last one (but make things a bit clearer)
# explicitly define a function first, and then return it
f_sum_new <- function(f) {
  
  # define a function
  f_sum_new_helper <- function(a, b) {
    
    result = 0
    while (a <= b) {
      result = result + f(a)
      a = a + 1L
    }
    result
  }
  
  # return the function
  f_sum_new_helper
}

sum_squares_new <- f_sum_new(function(x) {x ^ 2})
sum_cubes_new <- f_sum_new(function(x) {x ^ 3})

print(sum_squares_new(1, 3))
## [1] 14
print(sum_cubes_new(1, 3))
## [1] 36
# of course, you can do the following as well
print(f_sum_new(function(x) {x ^ 2})(1, 3))
## [1] 14

One import concept that makes function factories work is closure, or the enclosing environment of the returned function. Without it, none of the above will work. Let me not get into closure in this Bootcamp. You will learn it yourself if you want to dive deeper into R.

As a R beginner, perhaps you won’t write function factories yourself yet. However, you will see many useful R packages use this idea, so it’s good to know about it.

library(scales)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:purrr':
## 
##     discard
y <- c(12345, 123456, 1234567)

# comma_format() returns a function, and then apply it to y
comma_format()(y)
## [1] "12,345"    "123,456"   "1,234,567"

References

  1. Iteration chapter and Functions chapter in R for Data Science.

  2. Control flow chapter, Functions chapter, and Functional programming chapter in Advanced R