How do you iterate a vector?

purrr
constructive
reduce
accumulate
loop
iteration
Author
Affiliations

Layal Christine Lettry

cynkra GmbH

University of Fribourg, Dept. of Informatics, ASAM Group

Published

September 9, 2024

What functions can you use to replace a loop?

Initial object

Let’s assume that we have a numeric vector.

my_vec <- c("first" = 1L, "second" = 2L, "third" = 3L, "fourth" = 4L)
constructive::construct(my_vec)
c(first = 1L, second = 2L, third = 3L, fourth = 4L)

Use reduce() from purrr

Let’s say we want to compute the sum and the product of all the elements of our vector. We can use the reduce() function.

my_vec |>
  purrr::reduce(`+`)
[1] 10
my_vec |>
  purrr::reduce(`*`)
[1] 24

It is the same as doing

my_vec |> sum()
[1] 10
my_vec |> prod()
[1] 24

We could also write a loop for the sum, which would perform exactly the same operations as reduce(), but in a more complicated way.

# Define the function f
f <- function(x, y) {
  x + y
}

# Start with the first value
my_vec <- unname(my_vec)
result <- my_vec[1]

# Loop through the rest of the values
for (i in 2:length(my_vec)) {
  result <- f(result, my_vec[i])
}

print(result)
[1] 10

These results show that reduce() takes the vector values and applies the + or * function iteratively in the forward direction (default).

Difference between the forward and the backward direction

Let the function f be defined by f(a, b) = a + b. Then, reduce() will apply this to my_vec, i.e.

\[ f(f(f(1, 2), 3), 4) = f(f(3, 3), 4) = f(6, 4) = 10. \]

If the direction had been set backwards, the following would have occurred:

\[ f(f(f(4, 3), 2), 1) = f(f(7, 2), 1) = f(9, 1) = 10. \]

To see the difference between forward and backward, let’s subtract the elements of our vector iteratively.

my_vec |>
  purrr::reduce(`-`)
[1] -8
my_vec |>
  purrr::reduce(`-`, .dir = "backward")
[1] -2

Let the function g be defined by g(a, b) = a - b. Then, the forward subtraction corresponds to this operation:

\[ g(g(g(1, 2), 3), 4) = g(g(-1, 3), 4) = g(-4, 4) = -8. \]

The the backward subtraction would be:

\[ g(g(g(4, 3), 2), 1) = g(g(1, 2), 1) = g(-1, 1) = -2. \]

Initial value of the accumulation

We could set the initial value of the accumulation.

my_vec[[3]]
[1] 3
my_vec |>
  purrr::reduce(`-`, .init = my_vec[[3]])
[1] -7

Here, there is one additional argument and the iteration starts with the value of 3 that corresponds to my_vec[[3]]:

\[ g(g(g(g(3, 1), 2), 3), 4) = g(g(g(2, 2), 3), 4) = g(g(0, 3), 4) = g(-3, 4) = -7. \]

Apply a predefined function iteratively

With two arguments

Let’s say we want to compute the empiric variance for our vector.

We usually compute it with var(my_vec) = 1.6666667. This is an unbiased estimator of the variance because the denominator is n-1 = length(my_vec) - 1 = 3. The following operation:

1 / (length(my_vec) - 1) * sum((my_vec - mean(my_vec))^2)
[1] 1.666667

corresponds to

\[ 1/(n-1) \sum_{i = 1}^n (x_i - \bar{x})^2. \] However, it does not work when we try to apply this formula using reduce(), because we need to supply two arguments.

# does not work
my_vec |>
  purrr::reduce(\(x) 1 / (length(x) - 1) * sum((x - mean(x))^2))
Error in fn(out, elt, ...): unused argument (elt)

Let’s do it by entering two arguments into the reduce() function.

# works but is not the variance of my_vec
my_vec |>
  purrr::reduce(\(x, y) var(c(x, y)))
[1] 0.3828125
# or
my_var <- function(x, y) {
  1 / (length(c(x, y)) - 1) * sum((c(x, y) - mean(c(x, y)))^2)
}
my_vec |>
  purrr::reduce(\(x, y) my_var(x, y))
[1] 0.3828125

So, what does reduce() compute?

Let \(x =\) my_vec and \(h(x_1, x_2)\) be a function defined by

\[ h(x_1, x_2) = var(x_1, x_2) = 1/(2-1) \sum_{i = 1}^2 (x_i - \bar{x})^2 = \left[x_1 - \frac{(x_1 + x_2)}{2}\right]^2 + \left[x_2 - \frac{(x_1 + x_2)}{2}\right]^2. \] Thus, we get 0.3828125 because reduce() computes \(h(h(h(1, 2), 3), 4)\) which is equal to

var(c(var(c(var(my_vec[1:2]), my_vec[3])), my_vec[4]))
[1] 0.3828125
# or

my_var(my_var(my_var(my_vec[[1]], my_vec[[2]]), my_vec[[3]]), my_vec[[4]])
[1] 0.3828125

With three arguments

If .init is not specified, the function reduce2() takes as arguments a first vector and a second one which is shorter than the former by one element. Otherwise, both arguments should have the same number of of elements.

my_x <- my_vec
my_y <- my_vec

purrr::reduce2(my_x, my_y, paste, .init = 5)
[1] "5 1 1 2 2 3 3 4 4"
my_x <- my_vec |> unname()
my_y <- my_x[-length(my_x)] + 6

purrr::reduce2(my_x, my_y, paste)
[1] "1 2 7 3 8 4 9"

If .init is not specified, the reduce2() function takes as the initial value the first element of the first argument.

Otherwise, the argument .init gives the first element. This is the first argument.

Then, the values going from the first non-used element of the two arguments (here my_x and my_y) will be the second and the third elements.

Use accumulate() from purrr

The function accumulate() will give the intermediate results of the reduce() function.

my_vec |>
  purrr::accumulate(`+`)
[1]  1  3  6 10
my_vec |>
  purrr::accumulate(`*`)
[1]  1  2  6 24
my_vec |>
  purrr::accumulate(\(x, y) var(c(x, y)))
[1] 1.0000000 0.5000000 3.1250000 0.3828125

You can alsow use accumulate2() if you use 2 lists.

my_x <- my_vec
my_y <- my_vec

purrr::accumulate2(my_x, my_y, paste, .init = 5)
[[1]]
[1] 5

[[2]]
[1] "5 1 1"

[[3]]
[1] "5 1 1 2 2"

[[4]]
[1] "5 1 1 2 2 3 3"

[[5]]
[1] "5 1 1 2 2 3 3 4 4"
my_x <- my_vec |> unname()
my_y <- my_x[-length(my_x)] + 6

purrr::accumulate2(my_x, my_y, paste)
[[1]]
[1] 1

[[2]]
[1] "1 2 7"

[[3]]
[1] "1 2 7 3 8"

[[4]]
[1] "1 2 7 3 8 4 9"

Citation

BibTeX citation:
@online{lettry2024,
  author = {Lettry, Layal Christine},
  title = {How Do You Iterate a Vector?},
  date = {2024-09-09},
  url = {https://rdiscovery.netlify.app/posts/2024-09-09_reduce/},
  langid = {en}
}
For attribution, please cite this work as:
Lettry, Layal Christine. 2024. “How Do You Iterate a Vector?” September 9, 2024. https://rdiscovery.netlify.app/posts/2024-09-09_reduce/.