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