1#' Modify elements selectively
2#'
3#' @description
4#'
5#' Unlike [map()] and its variants which always return a fixed object
6#' type (list for `map()`, integer vector for `map_int()`, etc), the
7#' `modify()` family always returns the same type as the input object.
8#'
9#' * `modify()` is a shortcut for `x[[i]] <- f(x[[i]]);
10#'   return(x)`.
11#'
12#' * `modify_if()` only modifies the elements of `x` that satisfy a
13#'   predicate and leaves the others unchanged. `modify_at()` only
14#'   modifies elements given by names or positions.
15#'
16#' * `modify2()` modifies the elements of `.x` but also passes the
17#'   elements of `.y` to `.f`, just like [map2()]. `imodify()` passes
18#'   the names or the indices to `.f` like [imap()] does.
19#'
20#' * `modify_depth()` only modifies elements at a given level of a
21#'   nested data structure.
22#'
23#' * [modify_in()] modifies a single element in a [pluck()] location.
24#'
25#' @inheritParams map
26#' @inheritParams map2
27#' @param .depth Level of `.x` to map on. Use a negative value to count up
28#'   from the lowest level of the list.
29#'
30#'   * `modify_depth(x, 0, fun)` is equivalent to `x[] <- fun(x)`.
31#'   * `modify_depth(x, 1, fun)` is equivalent to `x <- modify(x, fun)`
32#'   * `modify_depth(x, 2, fun)` is equivalent to `x <- modify(x, ~ modify(., fun))`
33#' @return An object the same class as `.x`
34#'
35#' @details
36#'
37#' Since the transformation can alter the structure of the input; it's
38#' your responsibility to ensure that the transformation produces a
39#' valid output. For example, if you're modifying a data frame, `.f`
40#' must preserve the length of the input.
41#'
42#' @section Genericity:
43#'
44#' `modify()` and variants are generic over classes that implement
45#' `length()`, `[[` and `[[<-` methods. If the default implementation
46#' is not compatible for your class, you can override them with your
47#' own methods.
48#'
49#' If you implement your own `modify()` method, make sure it satisfies
50#' the following invariants:
51#'
52#' ```
53#' modify(x, identity) === x
54#' modify(x, compose(f, g)) === modify(x, g) %>% modify(f)
55#' ```
56#'
57#' These invariants are known as the [functor
58#' laws](https://wiki.haskell.org/Functor#Functor_Laws) in computer
59#' science.
60#'
61#'
62#' @family map variants
63#' @examples
64#' # Convert factors to characters
65#' iris %>%
66#'   modify_if(is.factor, as.character) %>%
67#'   str()
68#'
69#' # Specify which columns to map with a numeric vector of positions:
70#' mtcars %>% modify_at(c(1, 4, 5), as.character) %>% str()
71#'
72#' # Or with a vector of names:
73#' mtcars %>% modify_at(c("cyl", "am"), as.character) %>% str()
74#'
75#' list(x = rbernoulli(100), y = 1:100) %>%
76#'   transpose() %>%
77#'   modify_if("x", ~ update_list(., y = ~ y * 100)) %>%
78#'   transpose() %>%
79#'   simplify_all()
80#'
81#' # Use modify2() to map over two vectors and preserve the type of
82#' # the first one:
83#' x <- c(foo = 1L, bar = 2L)
84#' y <- c(TRUE, FALSE)
85#' modify2(x, y, ~ if (.y) .x else 0L)
86#'
87#' # Use a predicate function to decide whether to map a function:
88#' modify_if(iris, is.factor, as.character)
89#'
90#' # Specify an alternative with the `.else` argument:
91#' modify_if(iris, is.factor, as.character, .else = as.integer)
92#'
93#'
94#' # Modify at specified depth ---------------------------
95#' l1 <- list(
96#'   obj1 = list(
97#'     prop1 = list(param1 = 1:2, param2 = 3:4),
98#'     prop2 = list(param1 = 5:6, param2 = 7:8)
99#'   ),
100#'   obj2 = list(
101#'     prop1 = list(param1 = 9:10, param2 = 11:12),
102#'     prop2 = list(param1 = 12:14, param2 = 15:17)
103#'   )
104#' )
105#'
106#' # In the above list, "obj" is level 1, "prop" is level 2 and "param"
107#' # is level 3. To apply sum() on all params, we map it at depth 3:
108#' l1 %>% modify_depth(3, sum) %>% str()
109#'
110#' # Note that vectorised operations will yield the same result when
111#' # applied at the list level as when applied at the atomic result.
112#' # The former is more efficient because it takes advantage of
113#' # vectorisation.
114#' l1 %>% modify_depth(3, `+`, 100L)
115#' l1 %>% modify_depth(4, `+`, 100L)
116#'
117#' # modify() lets us pluck the elements prop1/param2 in obj1 and obj2:
118#' l1 %>% modify(c("prop1", "param2")) %>% str()
119#'
120#' # But what if we want to pluck all param2 elements? Then we need to
121#' # act at a lower level:
122#' l1 %>% modify_depth(2, "param2") %>% str()
123#'
124#' # modify_depth() can be with other purrr functions to make them operate at
125#' # a lower level. Here we ask pmap() to map paste() simultaneously over all
126#' # elements of the objects at the second level. paste() is effectively
127#' # mapped at level 3.
128#' l1 %>% modify_depth(2, ~ pmap(., paste, sep = " / ")) %>% str()
129#' @export
130modify <- function(.x, .f, ...) {
131  UseMethod("modify")
132}
133#' @rdname modify
134#' @export
135modify.default <- function(.x, .f, ...) {
136  .f <- as_mapper(.f, ...)
137
138  for (i in seq_along(.x)) {
139    .x[[i]] <- .f(.x[[i]], ...)
140  }
141
142  .x
143}
144
145#' @rdname modify
146#' @inheritParams map_if
147#' @export
148modify_if <- function(.x, .p, .f, ..., .else = NULL) {
149  UseMethod("modify_if")
150}
151#' @rdname modify
152#' @export
153modify_if.default <- function(.x, .p, .f, ..., .else = NULL) {
154  sel <- probe(.x, .p)
155  index <- seq_along(.x)
156
157  .f <- as_mapper(.f, ...)
158  for (i in index[sel]) {
159    .x[[i]] <- .f(.x[[i]], ...)
160  }
161
162  if (!is_null(.else)) {
163    .else <- as_mapper(.else, ...)
164    for (i in index[!sel]) {
165      .x[[i]] <- .else(.x[[i]], ...)
166    }
167  }
168
169  .x
170}
171
172#' @rdname modify
173#' @inheritParams map_at
174#' @export
175modify_at <- function(.x, .at, .f, ...) {
176  UseMethod("modify_at")
177}
178#' @rdname modify
179#' @export
180modify_at.default <- function(.x, .at, .f, ...) {
181  where <- at_selection(names(.x), .at)
182  sel <- inv_which(.x, where)
183  modify_if(.x, sel, .f, ...)
184}
185
186# TODO: Replace all the following methods with a generic strategy that
187# implements sane coercion rules for base vectors
188
189#' @export
190modify.integer  <- function (.x, .f, ...) {
191  .x[] <- map_int(.x, .f, ...)
192  .x
193}
194#' @export
195modify.double  <- function (.x, .f, ...) {
196  .x[] <- map_dbl(.x, .f, ...)
197  .x
198}
199#' @export
200modify.character  <- function (.x, .f, ...) {
201  .x[] <- map_chr(.x, .f, ...)
202  .x
203}
204#' @export
205modify.logical  <- function (.x, .f, ...) {
206  .x[] <- map_lgl(.x, .f, ...)
207  .x
208}
209#' @export
210modify.pairlist <- function(.x, .f, ...) {
211  as.pairlist(map(.x, .f, ...))
212}
213
214#' @export
215modify_if.integer <- function(.x, .p, .f, ...) {
216  sel <- probe(.x, .p)
217  .x[sel] <- map_int(.x[sel], .f, ...)
218  .x
219}
220#' @export
221modify_if.double <- function(.x, .p, .f, ...) {
222  sel <- probe(.x, .p)
223  .x[sel] <- map_dbl(.x[sel], .f, ...)
224  .x
225}
226#' @export
227modify_if.character <- function(.x, .p, .f, ...) {
228  sel <- probe(.x, .p)
229  .x[sel] <- map_chr(.x[sel], .f, ...)
230  .x
231}
232#' @export
233modify_if.logical <- function(.x, .p, .f, ...) {
234  sel <- probe(.x, .p)
235  .x[sel] <- map_lgl(.x[sel], .f, ...)
236  .x
237}
238
239#' @export
240modify_at.integer <- function(.x, .at, .f, ...) {
241  where <- at_selection(names(.x), .at)
242  sel <- inv_which(.x, where)
243  .x[sel] <- map_int(.x[sel], .f, ...)
244  .x
245}
246#' @export
247modify_at.double <- function(.x, .at, .f, ...) {
248  where <- at_selection(names(.x), .at)
249  sel <- inv_which(.x, where)
250  .x[sel] <- map_dbl(.x[sel], .f, ...)
251  .x
252}
253#' @export
254modify_at.character <- function(.x, .at, .f, ...) {
255  where <- at_selection(names(.x), .at)
256  sel <- inv_which(.x, where)
257  .x[sel] <- map_chr(.x[sel], .f, ...)
258  .x
259}
260#' @export
261modify_at.logical <- function(.x, .at, .f, ...) {
262  where <- at_selection(names(.x), .at)
263  sel <- inv_which(.x, where)
264  .x[sel] <- map_lgl(.x[sel], .f, ...)
265  .x
266}
267
268#' Modify a pluck location
269#'
270#' @description
271#'
272#' * `assign_in()` takes a data structure and a [pluck][pluck] location,
273#'   assigns a value there, and returns the modified data structure.
274#'
275#' * `modify_in()` applies a function to a pluck location, assigns the
276#'   result back to that location with [assign_in()], and returns the
277#'   modified data structure.
278#'
279#' The pluck location must exist.
280#'
281#' @inheritParams pluck
282#' @param .f A function to apply at the pluck location given by `.where`.
283#' @param ... Arguments passed to `.f`.
284#' @param .where,where A pluck location, as a numeric vector of
285#'   positions, a character vector of names, or a list combining both.
286#'   The location must exist in the data structure.
287#'
288#' @seealso [pluck()]
289#' @examples
290#' # Recall that pluck() returns a component of a data structure that
291#' # might be arbitrarily deep
292#' x <- list(list(bar = 1, foo = 2))
293#' pluck(x, 1, "foo")
294#'
295#' # Use assign_in() to modify the pluck location:
296#' assign_in(x, list(1, "foo"), 100)
297#'
298#' # modify_in() applies a function to that location and update the
299#' # element in place:
300#' modify_in(x, list(1, "foo"), ~ .x * 200)
301#'
302#' # Additional arguments are passed to the function in the ordinary way:
303#' modify_in(x, list(1, "foo"), `+`, 100)
304#' @export
305modify_in <- function(.x, .where, .f, ...) {
306  .where <- as.list(.where)
307  .f <- rlang::as_function(.f)
308
309  value <- .f(chuck(.x, !!!.where), ...)
310  assign_in(.x, .where, value)
311}
312#' @rdname modify_in
313#' @param value A value to replace in `.x` at the pluck location.
314#' @export
315assign_in <- function(x, where, value) {
316  # Check value exists at pluck location
317  chuck(x, !!!where)
318
319  call <- reduce_subset_call(quote(x), as.list(where))
320  call <- call("<-", call, value)
321  eval_bare(call)
322  x
323}
324
325#' @rdname modify
326#' @export
327modify2 <- function(.x, .y, .f, ...) {
328  UseMethod("modify2")
329}
330#' @export
331modify2.default <- function(.x, .y, .f, ...) {
332  .f <- as_mapper(.f, ...)
333
334  args <- recycle_args(list(.x, .y))
335  .x <- args[[1]]
336  .y <- args[[2]]
337
338  for (i in seq_along(.x)) {
339    .x[[i]] <- .f(.x[[i]], .y[[i]], ...)
340  }
341
342  .x
343}
344#' @rdname modify
345#' @export
346imodify <- function(.x, .f, ...) {
347  modify2(.x, vec_index(.x), .f, ...)
348}
349
350# TODO: Improve genericity (see above)
351#' @export
352modify2.integer  <- function(.x, .y, .f, ...) {
353  modify_base(map2_int, .x, .y, .f, ...)
354}
355#' @export
356modify2.double  <- function(.x, .y, .f, ...) {
357  modify_base(map2_dbl, .x, .y, .f, ...)
358}
359#' @export
360modify2.character  <- function(.x, .y, .f, ...) {
361  modify_base(map2_chr, .x, .y, .f, ...)
362}
363#' @export
364modify2.logical  <- function(.x, .y, .f, ...) {
365  modify_base(map2_lgl, .x, .y, .f, ...)
366}
367
368modify_base <- function(mapper, .x, .y, .f, ...) {
369  args <- recycle_args(list(.x, .y))
370  .x <- args[[1]]
371  .y <- args[[2]]
372
373  .x[] <- mapper(.x, .y, .f, ...)
374  .x
375}
376
377#' @rdname modify
378#' @export
379modify_depth <- function(.x, .depth, .f, ..., .ragged = .depth < 0) {
380  if (!is_integerish(.depth, n = 1, finite = TRUE)) {
381    abort("`.depth` must be a single number")
382  }
383  UseMethod("modify_depth")
384}
385#' @rdname modify
386#' @export
387modify_depth.default <- function(.x, .depth, .f, ..., .ragged = .depth < 0) {
388  force(.ragged)
389
390  if (.depth < 0) {
391    .depth <- vec_depth(.x) + .depth
392  }
393
394  .f <- as_mapper(.f, ...)
395  modify_depth_rec(.x, .depth, .f, ..., .ragged = .ragged, .atomic = FALSE)
396}
397
398modify_depth_rec <- function(.x, .depth, .f,
399                             ...,
400                             .ragged = FALSE,
401                             .atomic = FALSE) {
402  if (.depth < 0) {
403    abort("Invalid depth")
404  }
405
406  if (.atomic) {
407    if (!.ragged) {
408      abort("List not deep enough")
409    }
410    return(modify(.x, .f, ...))
411  }
412
413  if (.depth == 0) {
414    # TODO vctrs: Use `vec_cast()` on result?
415    .x[] <- .f(.x, ...)
416    return(.x)
417  }
418
419  if (.depth == 1) {
420    return(modify(.x, .f, ...))
421  }
422
423  # Should this be replaced with a generic way of figuring out atomic
424  # types?
425  .atomic <- is_atomic(.x)
426
427  modify(.x, function(x) {
428    modify_depth_rec(x, .depth - 1, .f, ..., .ragged = .ragged, .atomic = .atomic)
429  })
430}
431
432#' Map at depth
433#'
434#' This function is defunct and has been replaced by [map_depth()].
435#' See also [modify_depth()] for a version that preserves the types of
436#' the elements of the tree.
437#'
438#' @inheritParams map
439#' @inheritParams map_if
440
441#' @export
442#' @keywords internal
443at_depth <- function(.x, .depth, .f, ...) {
444  stop_defunct("at_depth() is defunct, please use `map_depth()` instead")
445}
446
447# Internal version of map_lgl() that works with logical vectors
448probe <- function(.x, .p, ...) {
449  if (is_logical(.p)) {
450    stopifnot(length(.p) == length(.x))
451    .p
452  } else {
453    .p <- as_predicate(.p, ..., .mapper = TRUE)
454    map_lgl(.x, .p, ...)
455  }
456}
457
458inv_which <- function(x, sel) {
459  if (is.character(sel)) {
460    names <- names(x)
461    if (is.null(names)) {
462      stop("character indexing requires a named object", call. = FALSE)
463    }
464    names %in% sel
465  } else if (is.numeric(sel)) {
466    if (any(sel < 0)) {
467      !seq_along(x) %in% abs(sel)
468    } else {
469      seq_along(x) %in% sel
470    }
471
472  } else {
473    stop("unrecognised index type", call. = FALSE)
474  }
475}
476