diff --git a/DESCRIPTION b/DESCRIPTION index 80cf5059..5db21e39 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,7 @@ Imports: lifecycle (>= 1.0.1.9001), magrittr (>= 1.5.0), rlang (>= 0.4.10), - vctrs (>= 0.3.2) + vctrs (>= 0.4.1.9000) Suggests: covr, dplyr (>= 0.7.8), @@ -36,5 +36,6 @@ LazyData: true Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.1 Config/testthat/edition: 3 -Remotes: - r-lib/lifecycle +Remotes: + r-lib/lifecycle, + r-lib/vctrs diff --git a/NAMESPACE b/NAMESPACE index 88c2dbea..38557f92 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -129,8 +129,12 @@ export(lift_lv) export(lift_vd) export(lift_vl) export(list_along) +export(list_c) +export(list_cbind) +export(list_flatten) export(list_merge) export(list_modify) +export(list_rbind) export(lmap) export(lmap_at) export(lmap_if) diff --git a/NEWS.md b/NEWS.md index 373604e3..4590752c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,7 +18,8 @@ manipulation that is very uncommon in R code (#871). * `splice()` is deprecated because we no longer believe that automatic - splicing makes for good UI. Instead use `list2()` + `!!!` (#869). + splicing makes for good UI. Instead use `list2()` + `!!!` or + `list_flatten()` (#869). * `as_function()`, `at_depth()`, and the `...f` argument to `partial()` are no longer supported. They have been defunct for quite some time. @@ -36,8 +37,17 @@ * `*_raw()` have been deprecated because they are of limited use and you can now use `map_vec()` instead (#903). +* `flatten()` and friends are all deprecated in favour of `list_flatten()`, + `list_c()`, `list_cbind()`, and `list_rbind()`. + +* `*_dfc()` and `*_dfr()` have been deprecated in favour of using the + appropriate map function along with `list_rbind()` or `list_cbind()` (#912). + ## Features and fixes +* New `list_c()`, `list_rbind()`, and `list_cbind()` make it easy to + `c()`, `rbind()`, or `cbind()` all of the elements in a list. + * `_lgl()`, `_int()`, `_int()`, and `_dbl()` now use the same (strict) coercion methods as vctrs (#904). This means that: diff --git a/R/arrays.R b/R/arrays.R index 6f8205fb..9e646914 100644 --- a/R/arrays.R +++ b/R/arrays.R @@ -59,7 +59,7 @@ array_branch <- function(array, margin = NULL) { } as.list(array) } else { - flatten(apply(array, margin, list)) + list_flatten(apply(array, margin, list)) } } diff --git a/R/flatten.R b/R/flatten.R index 62f8d26e..3a830d47 100644 --- a/R/flatten.R +++ b/R/flatten.R @@ -1,8 +1,15 @@ #' Flatten a list of lists into a simple vector. #' -#' These functions remove a level hierarchy from a list. They are similar to -#' [unlist()], but they only ever remove a single layer of hierarchy and they -#' are type-stable, so you always know what the type of the output is. +#' @description +#' `r lifecycle::badge("deprecated")` +#' +#' These functions have been deprecated because their behavior was inconsistent. +#' +#' * `flatten()` has been replaced by [list_flatten()]. +#' * `flatten_lgl()`, `flatten_int()`, `flatten_dbl()`, and `flatten_chr()` +#' have been replaced by [list_c()]. +#' * `flatten_dfr()` and `flatten_dfc()` have been replaced by [list_rbind()] +#' and [list_cbind()] respectively. #' #' @param .x A list to flatten. The contents of the list can be anything for #' `flatten()` (as a list is returned), but the contents must match the @@ -14,43 +21,53 @@ #' `flatten_dfr()` and `flatten_dfc()` return data frames created by #' row-binding and column-binding respectively. They require dplyr to #' be installed. +#' @keywords internal #' @inheritParams map #' @export #' @examples -#' x <- rerun(2, sample(4)) +#' x <- map(1:3, ~ sample(4)) #' x -#' x %>% flatten() -#' x %>% flatten_int() #' -#' # You can use flatten in conjunction with map -#' x %>% map(1L) %>% flatten_int() -#' # But it's more efficient to use the typed map instead. -#' x %>% map_int(1L) +#' # was +#' x %>% flatten_int() %>% str() +#' # now +#' x %>% list_c() %>% str() +#' +#' x <- list(list(1, 2), list(3, 4)) +#' # was +#' x %>% flatten() %>% str() +#' # now +#' x %>% list_flatten() %>% str() flatten <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten()", "list_flatten()") .Call(flatten_impl, .x) } #' @export #' @rdname flatten flatten_lgl <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten_lgl()", "list_c()") .Call(vflatten_impl, .x, "logical") } #' @export #' @rdname flatten flatten_int <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten_lgl()", "list_c()") .Call(vflatten_impl, .x, "integer") } #' @export #' @rdname flatten flatten_dbl <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten_lgl()", "list_c()") .Call(vflatten_impl, .x, "double") } #' @export #' @rdname flatten flatten_chr <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten_lgl()", "list_c()") .Call(vflatten_impl, .x, "character") } @@ -58,6 +75,7 @@ flatten_chr <- function(.x) { #' @export #' @rdname flatten flatten_dfr <- function(.x, .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "flatten_dfr()", "list_rbind()") check_installed("dplyr", "for `flatten_dfr()`.") res <- .Call(flatten_impl, .x) @@ -67,6 +85,7 @@ flatten_dfr <- function(.x, .id = NULL) { #' @export #' @rdname flatten flatten_dfc <- function(.x) { + lifecycle::deprecate_warn("0.4.0", "flatten_dfc()", "list_cbind()") check_installed("dplyr", "for `flatten_dfc()`.") res <- .Call(flatten_impl, .x) @@ -76,4 +95,10 @@ flatten_dfc <- function(.x) { #' @export #' @rdname flatten #' @usage NULL -flatten_df <- flatten_dfr +flatten_df <- function(.x, .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "flatten_df()", "list_rbind()") + check_installed("dplyr", "for `flatten_dfr()`.") + + res <- .Call(flatten_impl, .x) + dplyr::bind_rows(res, .id = .id) +} diff --git a/R/imap.R b/R/imap.R index d79e8c45..e918f35f 100644 --- a/R/imap.R +++ b/R/imap.R @@ -59,20 +59,6 @@ imap_dbl <- function(.x, .f, ...) { } -#' @rdname imap -#' @export -imap_dfr <- function(.x, .f, ..., .id = NULL) { - .f <- as_mapper(.f, ...) - map2_dfr(.x, vec_index(.x), .f, ..., .id = .id) -} - -#' @rdname imap -#' @export -imap_dfc <- function(.x, .f, ...) { - .f <- as_mapper(.f, ...) - map2_dfc(.x, vec_index(.x), .f, ...) -} - #' @export #' @rdname imap iwalk <- function(.x, .f, ...) { diff --git a/R/list-combine.R b/R/list-combine.R new file mode 100644 index 00000000..349f05a8 --- /dev/null +++ b/R/list-combine.R @@ -0,0 +1,79 @@ +#' Combine list elements into a single data structure +#' +#' @description +#' * `list_c()` combines elements into a vector by concatenating them together +#' with [vctrs::vec_c()]. +#' +#' * `list_rbind()` combines elements into a data frame by row-binding them +#' together with [vctrs::vec_rbind()]. +#' +#' * `list_cbind()` combines elements into a data frame by column-binding them +#' together with [vctrs::vec_cbind()]. +#' +#' @param x A list. For `list_rbind()` and `list_cbind()` the list must +#' only contain data frames. +#' @param ptype An optional prototype to ensure that the output type is always +#' the same. +#' @param id By default, `names(x)` are lost. Alternatively, supply a string +#' and the names will be saved into a column with name `{id}`. If `id` +#' is supplied and `x` is not named, the position of the elements will +#' be used instead of the names. +#' @param size An optional integer size to ensure that every input has the +#' same size (i.e. number of rows). +#' @param name_repair One of `"unique"`, `"universal"`, or `"check_unique"`. +#' See [vctrs::vec_as_names()] for the meaning of these options. +#' @export +#' @examples +#' x1 <- list(a = 1, b = 2, c = 3) +#' list_c(x1) +#' +#' x2 <- list( +#' a = data.frame(x = 1:2), +#' b = data.frame(y = "a") +#' ) +#' list_rbind(x2) +#' list_rbind(x2, id = "id") +#' list_rbind(unname(x2), id = "id") +#' +#' list_cbind(x2) +list_c <- function(x, ptype = NULL) { + vec_check_list(x) + vctrs::vec_unchop(x, ptype = ptype) +} + +#' @export +#' @rdname list_c +list_cbind <- function( + x, + name_repair = c("unique", "universal", "check_unique"), + size = NULL + ) { + check_list_of_data_frames(x) + + vctrs::vec_cbind(!!!x, .name_repair = name_repair, .size = size, .call = current_env()) +} + +#' @export +#' @rdname list_c +list_rbind <- function(x, id = rlang::zap(), ptype = NULL) { + check_list_of_data_frames(x) + + vctrs::vec_rbind(!!!x, .names_to = id, .ptype = ptype, .call = current_env()) +} + + +check_list_of_data_frames <- function(x, error_call = caller_env()) { + vec_check_list(x, call = error_call) + + is_df <- map_lgl(x, is.data.frame) + + if (all(is_df)) { + return() + } + + bad <- which(!is_df) + cli::cli_abort( + "All elements of {.arg x} must be data frames. Elements {bad} are not.", + call = error_call + ) +} diff --git a/R/list-flatten.R b/R/list-flatten.R new file mode 100644 index 00000000..0699b48f --- /dev/null +++ b/R/list-flatten.R @@ -0,0 +1,55 @@ +#' Flatten a list +#' +#' Flattening a list removes a single layer of internal hierarchy, +#' i.e. it inlines elements that are lists leaving non-lists alone. +#' +#' @param x A list. +#' @param name_spec If both inner and outer names are present, control +#' how they are combined. Should be a glue specification that uses +#' variables `inner` and `outer`. +#' @param name_repair One of `"minimal"`, `"unique"`, `"universal"`, or +#' `"check_unique"`. See [vctrs::vec_as_names()] for the meaning of these +#' options. +#' @return A list. The list might be shorter if `x` contains empty lists, +#' the same length if it contains lists of length 1 or no sub-lists, +#' or longer if it contains lists of length > 1. +#' @export +#' @examples +#' x <- list(1, list(2, 3), list(4, list(5))) +#' x %>% list_flatten() %>% str() +#' x %>% list_flatten() %>% list_flatten() %>% str() +#' +#' # Flat lists are left as is +#' list(1, 2, 3, 4, 5) %>% list_flatten() %>% str() +#' +#' # Empty lists will disappear +#' list(1, list(), 2, list(3)) %>% list_flatten() %>% str() +#' +#' # Another way to see this is that it reduces the depth of the list +#' x <- list( +#' list(), +#' list(list()) +#' ) +#' x %>% pluck_depth() +#' x %>% list_flatten() %>% pluck_depth() +#' +#' # Use name_spec to control how inner and outer names are combined +#' x <- list(x = list(a = 1, b = 2), y = list(c = 1, d = 2)) +#' x %>% list_flatten() %>% names() +#' x %>% list_flatten(name_spec = "{outer}") %>% names() +#' x %>% list_flatten(name_spec = "{inner}") %>% names() +list_flatten <- function( + x, + name_spec = "{outer}_{inner}", + name_repair = c("minimal", "unique", "check_unique", "universal") + ) { + vec_check_list(x) + + x <- map_if(x, vec_is_list, identity, .else = list) + vec_unchop( + x, + ptype = list(), + name_spec = name_spec, + name_repair = name_repair + ) +} diff --git a/R/lmap.R b/R/lmap.R index 56158d9a..561bc0c0 100644 --- a/R/lmap.R +++ b/R/lmap.R @@ -19,7 +19,8 @@ #' @inheritParams map_if #' @inheritParams map_at #' @inheritParams map -#' @return A list. There are no guarantees about the length. +#' @return A list or data frame, matching `.x`. There are no guarantees about +#' the length. #' @family map variants #' @export #' @examples @@ -86,5 +87,9 @@ lmap_helper <- function(.x, .ind, .f, ..., .else = NULL) { out[[i]] <- res } - flatten(out) + if (is.data.frame(.x)) { + list_cbind(out) + } else { + list_flatten(out) + } } diff --git a/R/map-df.R b/R/map-df.R new file mode 100644 index 00000000..dbefd6b3 --- /dev/null +++ b/R/map-df.R @@ -0,0 +1,176 @@ +#' Functions that return data frames +#' +#' @description +#' `r lifecycle::badge("deprecated")` +#' +#' These variants of [map()], [map2()], [imap()], and [pmap()] return data +#' frames. They have been deprecated because they use `dplyr::bind_rows()` +#' and `dplyr::bind_cols()` which often have confusing semantics, and their +#' names are suboptimal because they suggest they work like `_lgl()`, `_int()`, +#' and friends which require length 1 outputs, but actually they return results +#' of any size because the results are combined together without any size checks. +#' +#' Instead, we now recommend usin `map()`, `map2()`, etc with [list_rbind()] +#' and [list_cbind()]. These use [vctrs::vec_rbind()] and [vctrs::vec_cbind()] +#' under the hood, and have names that more clearly reflect their semantics. +#' +#' @param .id Either a string or `NULL`. If a string, the output will contain +#' a variable with that name, storing either the name (if `.x` is named) or +#' the index (if `.x` is unnamed) of the input. If `NULL`, the default, no +#' variable will be created. +#' +#' Only applies to `_dfr` variant. +#' @keywords internal +#' @export +#' @examples +#' # map --------------------------------------------- +#' # Was: +#' mtcars %>% +#' split(.$cyl) %>% +#' map(~ lm(mpg ~ wt, data = .x)) %>% +#' map_dfr(~ as.data.frame(t(as.matrix(coef(.))))) +#' +#' # Now: +#' mtcars %>% +#' split(.$cyl) %>% +#' map(~ lm(mpg ~ wt, data = .x)) %>% +#' map(~ as.data.frame(t(as.matrix(coef(.))))) %>% +#' list_rbind() +#' +#' # map2 --------------------------------------------- +#' +#' ex_fun <- function(arg1, arg2){ +#' col <- arg1 + arg2 +#' x <- as.data.frame(col) +#' } +#' arg1 <- 1:4 +#' arg2 <- 10:13 +#' +#' # was +#' map2_dfr(arg1, arg2, ex_fun) +#' # now +#' map2(arg1, arg2, ex_fun) %>% list_rbind() +#' +#' # was +#' map2_dfc(arg1, arg2, ex_fun) +#' # now +#' map2(arg1, arg2, ex_fun) %>% list_cbind() +map_dfr <- function(.x, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map_dfr()", I("`map()` + `list_rbind()`")) + check_installed("dplyr", "for `map_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- map(.x, .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @usage NULL +#' @export +map_df <- function(.x, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map_df()", I("`map()` + `list_rbind()`")) + check_installed("dplyr", "for `map_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- map(.x, .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @export +map_dfc <- function(.x, .f, ...) { + lifecycle::deprecate_warn("0.4.0", "map_dfc()", I("`map()` + `list_cbind()`")) + check_installed("dplyr", "for `map_dfc()`.") + + .f <- as_mapper(.f, ...) + res <- map(.x, .f, ...) + dplyr::bind_cols(res) +} + +#' @rdname map_dfr +#' @export +imap_dfr <- function(.x, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "imap_dfr()", I("`imap()` + `list_rbind()`")) + + .f <- as_mapper(.f, ...) + res <- map2(.x, vec_index(.x), .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @export +imap_dfc <- function(.x, .f, ...) { + lifecycle::deprecate_warn("0.4.0", "imap_dfc()", I("`imap()` + `list_cbind()`")) + + .f <- as_mapper(.f, ...) + res <- map2(.x, vec_index(.x), .f, ...) + dplyr::bind_cols(res) +} + +#' @rdname map_dfr +#' @export +map2_dfr <- function(.x, .y, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map2_dfr()", I("`map2()` + `list_rbind()`")) + check_installed("dplyr", "for `map2_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- map2(.x, .y, .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @export +map2_dfc <- function(.x, .y, .f, ...) { + lifecycle::deprecate_warn("0.4.0", "map2_dfc()", I("`map2()` + `list_cbind()`")) + check_installed("dplyr", "for `map2_dfc()`.") + + .f <- as_mapper(.f, ...) + res <- map2(.x, .y, .f, ...) + dplyr::bind_cols(res) +} + +#' @rdname map_dfr +#' @export +#' @usage NULL +map2_df <- function(.x, .y, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map2_df()", I("`map2()` + `list_rbind()`")) + check_installed("dplyr", "for `map2_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- map2(.x, .y, .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @export +pmap_dfr <- function(.l, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "pmap_dfr()", I("`pmap()` + `list_rbind()`")) + check_installed("dplyr", "for `pmap_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- pmap(.l, .f, ...) + dplyr::bind_rows(res, .id = .id) +} + +#' @rdname map_dfr +#' @export +pmap_dfc <- function(.l, .f, ...) { + lifecycle::deprecate_warn("0.4.0", "pmap_dfc()", I("`pmap()` + `list_cbind()`")) + check_installed("dplyr", "for `pmap_dfc()`.") + + .f <- as_mapper(.f, ...) + res <- pmap(.l, .f, ...) + dplyr::bind_cols(res) +} + +#' @rdname map_dfr +#' @export +#' @usage NULL +pmap_df <- function(.l, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "pmap_df()", I("`pmap()` + `list_rbind()`")) + check_installed("dplyr", "for `pmap_dfr()`.") + + .f <- as_mapper(.f, ...) + res <- pmap(.l, .f, ...) + dplyr::bind_rows(res, .id = .id) +} diff --git a/R/map-if-at.R b/R/map-if-at.R new file mode 100644 index 00000000..1f4ea758 --- /dev/null +++ b/R/map-if-at.R @@ -0,0 +1,143 @@ +#' Apply a function to each element of a vector conditionally +#' +#' @description +#' +#' The functions `map_if()` and `map_at()` take `.x` as input, apply +#' the function `.f` to some of the elements of `.x`, and return a +#' list of the same length as the input. +#' +#' * `map_if()` takes a predicate function `.p` as input to determine +#' which elements of `.x` are transformed with `.f`. +#' +#' * `map_at()` takes a vector of names or positions `.at` to specify +#' which elements of `.x` are transformed with `.f`. +#' +#' @inheritParams map +#' @param .p A single predicate function, a formula describing such a +#' predicate function, or a logical vector of the same length as `.x`. +#' Alternatively, if the elements of `.x` are themselves lists of +#' objects, a string indicating the name of a logical element in the +#' inner lists. Only those elements where `.p` evaluates to +#' `TRUE` will be modified. +#' @param .else A function applied to elements of `.x` for which `.p` +#' returns `FALSE`. +#' @export +#' @family map variants +#' @examples +#' # Use a predicate function to decide whether to map a function: +#' map_if(iris, is.factor, as.character) +#' +#' # Specify an alternative with the `.else` argument: +#' map_if(iris, is.factor, as.character, .else = as.integer) +#' +map_if <- function(.x, .p, .f, ..., .else = NULL) { + sel <- probe(.x, .p) + + out <- list_along(.x) + out[sel] <- map(.x[sel], .f, ...) + + if (is_null(.else)) { + out[!sel] <- .x[!sel] + } else { + out[!sel] <- map(.x[!sel], .else, ...) + } + + set_names(out, names(.x)) +} +#' @rdname map_if +#' @param .at A character vector of names, positive numeric vector of +#' positions to include, or a negative numeric vector of positions to +#' exlude. Only those elements corresponding to `.at` will be modified. +#' +#' `r lifecycle::badge("deprecated")`: if the tidyselect package is +#' installed, you can use `vars()` and tidyselect helpers to select +#' elements. +#' @examples +#' # Use numeric vector of positions select elements to change: +#' iris %>% map_at(c(4, 5), is.numeric) +#' +#' # Use vector of names to specify which elements to change: +#' iris %>% map_at("Species", toupper) +# +#' @export +map_at <- function(.x, .at, .f, ...) { + + where <- at_selection(names(.x), .at) + sel <- inv_which(.x, where) + + out <- list_along(.x) + out[sel] <- map(.x[sel], .f, ...) + out[!sel] <- .x[!sel] + + set_names(out, names(.x)) +} + + +#' @rdname map_if +#' @description * `map_depth()` allows to apply `.f` to a specific +#' depth level of a nested vector. +#' @param .depth Level of `.x` to map on. Use a negative value to +#' count up from the lowest level of the list. +#' +#' * `map_depth(x, 0, fun)` is equivalent to `fun(x)`. +#' * `map_depth(x, 1, fun)` is equivalent to `x <- map(x, fun)` +#' * `map_depth(x, 2, fun)` is equivalent to `x <- map(x, ~ map(., fun))` +#' @param .ragged If `TRUE`, will apply to leaves, even if they're not +#' at depth `.depth`. If `FALSE`, will throw an error if there are +#' no elements at depth `.depth`. +#' @examples +#' +#' # Use `map_depth()` to recursively traverse nested vectors and map +#' # a function at a certain depth: +#' x <- list(a = list(foo = 1:2, bar = 3:4), b = list(baz = 5:6)) +#' str(x) +#' map_depth(x, 2, paste, collapse = "/") +#' +#' # Equivalent to: +#' map(x, map, paste, collapse = "/") +#' @export +map_depth <- function(.x, .depth, .f, ..., .ragged = FALSE) { + if (!is_integerish(.depth, n = 1, finite = TRUE)) { + abort("`.depth` must be a single number") + } + if (.depth < 0) { + .depth <- pluck_depth(.x) + .depth + } + + .f <- as_mapper(.f, ...) + map_depth_rec(.x, .depth, .f, ..., .ragged = .ragged, .atomic = FALSE) +} + +map_depth_rec <- function(.x, + .depth, + .f, + ..., + .ragged, + .atomic) { + if (.depth < 0) { + abort("Invalid depth") + } + + if (.atomic) { + if (!.ragged) { + abort("List not deep enough") + } + return(map(.x, .f, ...)) + } + + if (.depth == 0) { + return(.f(.x, ...)) + } + + if (.depth == 1) { + return(map(.x, .f, ...)) + } + + # Should this be replaced with a generic way of figuring out atomic + # types? + .atomic <- is_atomic(.x) + + map(.x, function(x) { + map_depth_rec(x, .depth - 1, .f, ..., .ragged = .ragged, .atomic = .atomic) + }) +} diff --git a/R/map-raw.R b/R/map-raw.R index f143885b..e76df08b 100644 --- a/R/map-raw.R +++ b/R/map-raw.R @@ -1,7 +1,8 @@ -#' Function that return raw vectors +#' Functions that return raw vectors #' #' @description #' `r lifecycle::badge("deprecated")` +#' #' These variants of [map()], [map2()], [imap()], [pmap()], and [flatten()] #' return raw vectors. They have been deprecated because they are of limited #' use and you can now use `map_vec()` instead. diff --git a/R/map.R b/R/map.R index 48b653e0..25bcb985 100644 --- a/R/map.R +++ b/R/map.R @@ -1,24 +1,19 @@ -#' Apply a function to each element of a list or atomic vector +#' Apply a function to each element of a vector #' #' @description -#' #' The map functions transform their input by applying a function to -#' each element of a list or atomic vector and returning an object of the same length as the input. +#' each element of a list or atomic vector and returning an object of +#' the same length as the input. #' #' * `map()` always returns a list. See the [modify()] family for #' versions that return an object of the same type as the input. #' #' * `map_lgl()`, `map_int()`, `map_dbl()` and `map_chr()` return an -#' atomic vector of the indicated type (or die trying). -#' -#' * `map_dfr()` and `map_dfc()` return a data frame created by -#' row-binding and column-binding respectively. They require dplyr -#' to be installed. `map_df()` is an alias for `map_dfr()`. +#' atomic vector of the indicated type (or die trying). For these functions, +#' `.f` must return a length-1 vector of the appropriate type. #' -#' * The returned values of `.f` must be of length one for each element -#' of `.x`. If `.f` uses an extractor function shortcut, `.default` -#' can be specified to handle values that are absent or empty. See -#' [as_mapper()] for more on `.default`. +#' * `walk()` calls `.f` for its side-effect and returns +#' the input `.x`. #' #' @param .x A list or atomic vector. #' @param .f A function, specified in one of the following ways: @@ -38,17 +33,16 @@ #' Note that the arguments that differ in each call come before `.f`, #' and the arguments that are the same come after `.f`. #' @returns +#' The output length is determined by the length of the input. #' The output type is determined by the suffix: #' -#' * No suffix: returns a list the same length as the input. It will be -#' named if the input was named. +#' * No suffix: a list. #' #' * `_lgl`, `_int`, `_dbl`, `_chr` return a logical, integer, double, #' or character vector respectively. It will be named if the input was named. #' -#' * `_dfc` and `_dfr()` all return a data frame created by row-binding and -#' column-binding respectively. They require dplyr to be installed. -#' +#' * `walk()` returns the input `.x` (invisibly). This makes it easy to +#' use in a pipe. #' @export #' @family map variants #' @seealso [map_if()] for applying a function to only those elements @@ -110,89 +104,11 @@ #' map(~ lm(mpg ~ wt, data = .x)) %>% #' map(summary) %>% #' map_dbl("r.squared") -#' -#' # If each element of the output is a data frame, use -#' # map_dfr to row-bind them together: -#' mtcars %>% -#' split(.$cyl) %>% -#' map(~ lm(mpg ~ wt, data = .x)) %>% -#' map_dfr(~ as.data.frame(t(as.matrix(coef(.))))) -#' # (if you also want to preserve the variable names see -#' # the broom package) map <- function(.x, .f, ...) { .f <- as_mapper(.f, ...) .Call(map_impl, environment(), ".x", ".f", "list") } -#' Apply a function to each element of a vector conditionally -#' -#' @description -#' -#' The functions `map_if()` and `map_at()` take `.x` as input, apply -#' the function `.f` to some of the elements of `.x`, and return a -#' list of the same length as the input. -#' -#' * `map_if()` takes a predicate function `.p` as input to determine -#' which elements of `.x` are transformed with `.f`. -#' -#' * `map_at()` takes a vector of names or positions `.at` to specify -#' which elements of `.x` are transformed with `.f`. -#' -#' @inheritParams map -#' @inheritParams keep -#' @param .else A function applied to elements of `.x` for which `.p` -#' returns `FALSE`. -#' @export -#' @family map variants -#' @examples -#' # Use a predicate function to decide whether to map a function: -#' map_if(iris, is.factor, as.character) -#' -#' # Specify an alternative with the `.else` argument: -#' map_if(iris, is.factor, as.character, .else = as.integer) -#' -map_if <- function(.x, .p, .f, ..., .else = NULL) { - sel <- probe(.x, .p) - - out <- list_along(.x) - out[sel] <- map(.x[sel], .f, ...) - - if (is_null(.else)) { - out[!sel] <- .x[!sel] - } else { - out[!sel] <- map(.x[!sel], .else, ...) - } - - set_names(out, names(.x)) -} -#' @rdname map_if -#' @param .at A character vector of names, positive numeric vector of -#' positions to include, or a negative numeric vector of positions to -#' exlude. Only those elements corresponding to `.at` will be modified. -#' -#' `r lifecycle::badge("deprecated")`: if the tidyselect package is -#' installed, you can use `vars()` and tidyselect helpers to select -#' elements. -#' @examples -#' # Use numeric vector of positions select elements to change: -#' iris %>% map_at(c(4, 5), is.numeric) -#' -#' # Use vector of names to specify which elements to change: -#' iris %>% map_at("Species", toupper) -# -#' @export -map_at <- function(.x, .at, .f, ...) { - - where <- at_selection(names(.x), .at) - sel <- inv_which(.x, where) - - out <- list_along(.x) - out[sel] <- map(.x[sel], .f, ...) - out[!sel] <- .x[!sel] - - set_names(out, names(.x)) -} - #' @rdname map #' @export map_lgl <- function(.x, .f, ...) { @@ -221,116 +137,9 @@ map_dbl <- function(.x, .f, ...) { .Call(map_impl, environment(), ".x", ".f", "double") } - #' @rdname map -#' @param .id Either a string or `NULL`. If a string, the output will contain -#' a variable with that name, storing either the name (if `.x` is named) or -#' the index (if `.x` is unnamed) of the input. If `NULL`, the default, no -#' variable will be created. -#' -#' Only applies to `_dfr` variant. -#' @export -map_dfr <- function(.x, .f, ..., .id = NULL) { - check_installed("dplyr", "for `map_dfr()`.") - - .f <- as_mapper(.f, ...) - res <- map(.x, .f, ...) - dplyr::bind_rows(res, .id = .id) -} - -#' @rdname map -#' @export -#' @usage NULL -map_df <- map_dfr - -#' @rdname map -#' @export -map_dfc <- function(.x, .f, ...) { - check_installed("dplyr", "for `map_dfc()`.") - - .f <- as_mapper(.f, ...) - res <- map(.x, .f, ...) - dplyr::bind_cols(res) -} - -#' @rdname map -#' @description * `walk()` calls `.f` for its side-effect and returns -#' the input `.x`. -#' @return -#' -#' * `walk()` returns the input `.x` (invisibly). This makes it easy to -#' use in pipe. #' @export walk <- function(.x, .f, ...) { map(.x, .f, ...) invisible(.x) } - -#' @rdname map_if -#' @description * `map_depth()` allows to apply `.f` to a specific -#' depth level of a nested vector. -#' @param .depth Level of `.x` to map on. Use a negative value to -#' count up from the lowest level of the list. -#' -#' * `map_depth(x, 0, fun)` is equivalent to `fun(x)`. -#' * `map_depth(x, 1, fun)` is equivalent to `x <- map(x, fun)` -#' * `map_depth(x, 2, fun)` is equivalent to `x <- map(x, ~ map(., fun))` -#' @param .ragged If `TRUE`, will apply to leaves, even if they're not -#' at depth `.depth`. If `FALSE`, will throw an error if there are -#' no elements at depth `.depth`. -#' @examples -#' -#' # Use `map_depth()` to recursively traverse nested vectors and map -#' # a function at a certain depth: -#' x <- list(a = list(foo = 1:2, bar = 3:4), b = list(baz = 5:6)) -#' str(x) -#' map_depth(x, 2, paste, collapse = "/") -#' -#' # Equivalent to: -#' map(x, map, paste, collapse = "/") -#' @export -map_depth <- function(.x, .depth, .f, ..., .ragged = FALSE) { - if (!is_integerish(.depth, n = 1, finite = TRUE)) { - abort("`.depth` must be a single number") - } - if (.depth < 0) { - .depth <- pluck_depth(.x) + .depth - } - - .f <- as_mapper(.f, ...) - map_depth_rec(.x, .depth, .f, ..., .ragged = .ragged, .atomic = FALSE) -} - -map_depth_rec <- function(.x, - .depth, - .f, - ..., - .ragged, - .atomic) { - if (.depth < 0) { - abort("Invalid depth") - } - - if (.atomic) { - if (!.ragged) { - abort("List not deep enough") - } - return(map(.x, .f, ...)) - } - - if (.depth == 0) { - return(.f(.x, ...)) - } - - if (.depth == 1) { - return(map(.x, .f, ...)) - } - - # Should this be replaced with a generic way of figuring out atomic - # types? - .atomic <- is_atomic(.x) - - map(.x, function(x) { - map_depth_rec(x, .depth - 1, .f, ..., .ragged = .ragged, .atomic = .atomic) - }) -} diff --git a/R/map2.R b/R/map2.R index 2e16d977..7a782cd7 100644 --- a/R/map2.R +++ b/R/map2.R @@ -59,29 +59,7 @@ map2_chr <- function(.x, .y, .f, ...) { .Call(map2_impl, environment(), ".x", ".y", ".f", "character") } -#' @rdname map2 -#' @export -map2_dfr <- function(.x, .y, .f, ..., .id = NULL) { - check_installed("dplyr", "for `map2_dfr()`.") - - .f <- as_mapper(.f, ...) - res <- map2(.x, .y, .f, ...) - dplyr::bind_rows(res, .id = .id) -} - -#' @rdname map2 -#' @export -map2_dfc <- function(.x, .y, .f, ...) { - check_installed("dplyr", "for `map2_dfc()`.") - .f <- as_mapper(.f, ...) - res <- map2(.x, .y, .f, ...) - dplyr::bind_cols(res) -} -#' @rdname map2 -#' @export -#' @usage NULL -map2_df <- map2_dfr #' @export #' @rdname map2 walk2 <- function(.x, .y, .f, ...) { diff --git a/R/pmap.R b/R/pmap.R index dd686037..c49912a3 100644 --- a/R/pmap.R +++ b/R/pmap.R @@ -74,18 +74,6 @@ #' pmin(df$x, df$y) #' map2_dbl(df$x, df$y, min) #' pmap_dbl(df, min) -#' -#' # If you want to bind the results of your function rowwise, use: -#' # map2_dfr() or pmap_dfr() -#' ex_fun <- function(arg1, arg2){ -#' col <- arg1 + arg2 -#' x <- as.data.frame(col) -#' } -#' arg1 <- 1:4 -#' arg2 <- 10:13 -#' map2_dfr(arg1, arg2, ex_fun) -#' # If instead you want to bind by columns, use map2_dfc() or pmap_dfc() -#' map2_dfc(arg1, arg2, ex_fun) pmap <- function(.l, .f, ...) { .f <- as_mapper(.f, ...) if (is.data.frame(.l)) { @@ -136,31 +124,6 @@ pmap_chr <- function(.l, .f, ...) { .Call(pmap_impl, environment(), ".l", ".f", "character") } -#' @rdname pmap -#' @export -pmap_dfr <- function(.l, .f, ..., .id = NULL) { - check_installed("dplyr", "for `pmap_dfr()`.") - - .f <- as_mapper(.f, ...) - res <- pmap(.l, .f, ...) - dplyr::bind_rows(res, .id = .id) -} - -#' @rdname pmap -#' @export -pmap_dfc <- function(.l, .f, ...) { - check_installed("dplyr", "for `pmap_dfc()`.") - - .f <- as_mapper(.f, ...) - res <- pmap(.l, .f, ...) - dplyr::bind_cols(res) -} - -#' @rdname pmap -#' @export -#' @usage NULL -pmap_df <- pmap_dfr - #' @export #' @rdname pmap pwalk <- function(.l, .f, ...) { diff --git a/R/reduce.R b/R/reduce.R index dcb2cd34..62a76ae8 100644 --- a/R/reduce.R +++ b/R/reduce.R @@ -444,10 +444,11 @@ seq_len2 <- function(start, end) { #' library(dplyr) #' library(ggplot2) #' -#' rerun(5, rnorm(100)) %>% +#' map(1:5, ~ rnorm(100)) %>% #' set_names(paste0("sim", 1:5)) %>% #' map(~ accumulate(., ~ .05 + .x + .y)) %>% -#' map_dfr(~ tibble(value = .x, step = 1:100), .id = "simulation") %>% +#' map(~ tibble(value = .x, step = 1:100)) %>% +#' list_rbind(id = "simulation") %>% #' ggplot(aes(x = step, y = value)) + #' geom_line(aes(color = simulation)) + #' ggtitle("Simulations of a random walk with drift") diff --git a/R/splice.R b/R/splice.R index 87167aad..728890ff 100644 --- a/R/splice.R +++ b/R/splice.R @@ -5,8 +5,9 @@ #' #' This splices all arguments into a list. Non-list objects and lists #' with a S3 class are encapsulated in a list before concatenation. +#' #' We no longer believe that implicit/automatic splicing is a good idea; -#' instead use `!!!` in conjunction with `rlang::list2()`. +#' instead use `rlang::list2()` + `!!!` or [list_flatten()]. #' #' @param ... Objects to concatenate. #' @return A list. @@ -20,7 +21,7 @@ #' c(inputs, arg3 = c("c1", "c2")) %>% str() #' @export splice <- function(...) { - lifecycle::deprecate_warn("0.4.0", "splice()") + lifecycle::deprecate_warn("0.4.0", "splice()", "list_flatten()") splice_if(list(...), is_bare_list) } @@ -28,11 +29,5 @@ splice <- function(...) { splice_if <- function(.x, .p) { unspliced <- !probe(.x, .p) out <- modify_if(.x, unspliced, list) - - # Copy outer names to inner - if (!is.null(names(.x))) { - out[unspliced] <- map2(out[unspliced], names(out)[unspliced], set_names) - } - - flatten(out) + list_flatten(out, name_spec = "{inner}") } diff --git a/_pkgdown.yml b/_pkgdown.yml index bb9b8896..2fbf5c52 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -79,7 +79,8 @@ reference: A grab bag of useful tools for manipulating vectors. contents: - accumulate - - flatten + - list_c + - list_flatten - list_modify - reduce - transpose diff --git a/man/accumulate.Rd b/man/accumulate.Rd index 375cfa9d..7b1e0ed5 100644 --- a/man/accumulate.Rd +++ b/man/accumulate.Rd @@ -169,10 +169,11 @@ letters \%>\% accumulate(paste4) library(dplyr) library(ggplot2) -rerun(5, rnorm(100)) \%>\% +map(1:5, ~ rnorm(100)) \%>\% set_names(paste0("sim", 1:5)) \%>\% map(~ accumulate(., ~ .05 + .x + .y)) \%>\% - map_dfr(~ tibble(value = .x, step = 1:100), .id = "simulation") \%>\% + map(~ tibble(value = .x, step = 1:100)) \%>\% + list_rbind(id = "simulation") \%>\% ggplot(aes(x = step, y = value)) + geom_line(aes(color = simulation)) + ggtitle("Simulations of a random walk with drift") diff --git a/man/flatten.Rd b/man/flatten.Rd index eaf29626..2032cb5b 100644 --- a/man/flatten.Rd +++ b/man/flatten.Rd @@ -29,13 +29,6 @@ flatten_dfc(.x) \item{.x}{A list to flatten. The contents of the list can be anything for \code{flatten()} (as a list is returned), but the contents must match the type for the other functions.} - -\item{.id}{Either a string or \code{NULL}. If a string, the output will contain -a variable with that name, storing either the name (if \code{.x} is named) or -the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no -variable will be created. - -Only applies to \verb{_dfr} variant.} } \value{ \code{flatten()} returns a list, \code{flatten_lgl()} a logical @@ -47,18 +40,30 @@ row-binding and column-binding respectively. They require dplyr to be installed. } \description{ -These functions remove a level hierarchy from a list. They are similar to -\code{\link[=unlist]{unlist()}}, but they only ever remove a single layer of hierarchy and they -are type-stable, so you always know what the type of the output is. +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} + +These functions have been deprecated because their behavior was inconsistent. +\itemize{ +\item \code{flatten()} has been replaced by \code{\link[=list_flatten]{list_flatten()}}. +\item \code{flatten_lgl()}, \code{flatten_int()}, \code{flatten_dbl()}, and \code{flatten_chr()} +have been replaced by \code{\link[=list_c]{list_c()}}. +\item \code{flatten_dfr()} and \code{flatten_dfc()} have been replaced by \code{\link[=list_rbind]{list_rbind()}} +and \code{\link[=list_cbind]{list_cbind()}} respectively. +} } \examples{ -x <- rerun(2, sample(4)) +x <- map(1:3, ~ sample(4)) x -x \%>\% flatten() -x \%>\% flatten_int() -# You can use flatten in conjunction with map -x \%>\% map(1L) \%>\% flatten_int() -# But it's more efficient to use the typed map instead. -x \%>\% map_int(1L) +# was +x \%>\% flatten_int() \%>\% str() +# now +x \%>\% list_c() \%>\% str() + +x <- list(list(1, 2), list(3, 4)) +# was +x \%>\% flatten() \%>\% str() +# now +x \%>\% list_flatten() \%>\% str() } +\keyword{internal} diff --git a/man/head_while.Rd b/man/head_while.Rd index 26107be9..3ca9cccb 100644 --- a/man/head_while.Rd +++ b/man/head_while.Rd @@ -12,15 +12,12 @@ tail_while(.x, .p, ...) \arguments{ \item{.x}{A list or atomic vector.} -\item{.p}{A predicate function (i.e. a function that returns either \code{TRUE} -or \code{FALSE}) specified in one of the following ways: -\itemize{ -\item A named function, e.g. \code{is.character}. -\item An anonymous function, e.g. \verb{\\(x) all(x < 0)} or \code{function(x) all(x < 0)}. -\item A formula, e.g. \code{~ all(.x < 0)}. You must use \code{.x} to refer to the first -argument). Only recommended if you require backward compatibility with -older versions of R. -}} +\item{.p}{A single predicate function, a formula describing such a +predicate function, or a logical vector of the same length as \code{.x}. +Alternatively, if the elements of \code{.x} are themselves lists of +objects, a string indicating the name of a logical element in the +inner lists. Only those elements where \code{.p} evaluates to +\code{TRUE} will be modified.} \item{...}{Additional arguments passed on to the mapped function. diff --git a/man/imap.Rd b/man/imap.Rd index 70c55f4c..a2f07500 100644 --- a/man/imap.Rd +++ b/man/imap.Rd @@ -6,8 +6,6 @@ \alias{imap_chr} \alias{imap_int} \alias{imap_dbl} -\alias{imap_dfr} -\alias{imap_dfc} \alias{iwalk} \title{Apply a function to each element of a vector, and its index} \usage{ @@ -21,10 +19,6 @@ imap_int(.x, .f, ...) imap_dbl(.x, .f, ...) -imap_dfr(.x, .f, ..., .id = NULL) - -imap_dfc(.x, .f, ...) - iwalk(.x, .f, ...) } \arguments{ @@ -44,13 +38,6 @@ if you require backward compatibility with older versions of R. Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} - -\item{.id}{Either a string or \code{NULL}. If a string, the output will contain -a variable with that name, storing either the name (if \code{.x} is named) or -the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no -variable will be created. - -Only applies to \verb{_dfr} variant.} } \value{ A vector the same length as \code{.x}. diff --git a/man/list_c.Rd b/man/list_c.Rd new file mode 100644 index 00000000..d9b6b784 --- /dev/null +++ b/man/list_c.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/list-combine.R +\name{list_c} +\alias{list_c} +\alias{list_cbind} +\alias{list_rbind} +\title{Combine list elements into a single data structure} +\usage{ +list_c(x, ptype = NULL) + +list_cbind( + x, + name_repair = c("unique", "universal", "check_unique"), + size = NULL +) + +list_rbind(x, id = rlang::zap(), ptype = NULL) +} +\arguments{ +\item{x}{A list. For \code{list_rbind()} and \code{list_cbind()} the list must +only contain data frames.} + +\item{ptype}{An optional prototype to ensure that the output type is always +the same.} + +\item{name_repair}{One of \code{"unique"}, \code{"universal"}, or \code{"check_unique"}. +See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of these options.} + +\item{size}{An optional integer size to ensure that every input has the +same size (i.e. number of rows).} + +\item{id}{By default, \code{names(x)} are lost. Alternatively, supply a string +and the names will be saved into a column with name \code{{id}}. If \code{id} +is supplied and \code{x} is not named, the position of the elements will +be used instead of the names.} +} +\description{ +\itemize{ +\item \code{list_c()} combines elements into a vector by concatenating them together +with \code{\link[vctrs:vec_c]{vctrs::vec_c()}}. +\item \code{list_rbind()} combines elements into a data frame by row-binding them +together with \code{\link[vctrs:vec_bind]{vctrs::vec_rbind()}}. +\item \code{list_cbind()} combines elements into a data frame by column-binding them +together with \code{\link[vctrs:vec_bind]{vctrs::vec_cbind()}}. +} +} +\examples{ +x1 <- list(a = 1, b = 2, c = 3) +list_c(x1) + +x2 <- list( + a = data.frame(x = 1:2), + b = data.frame(y = "a") +) +list_rbind(x2) +list_rbind(x2, id = "id") +list_rbind(unname(x2), id = "id") + +list_cbind(x2) +} diff --git a/man/list_flatten.Rd b/man/list_flatten.Rd new file mode 100644 index 00000000..0a7b7cca --- /dev/null +++ b/man/list_flatten.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/list-flatten.R +\name{list_flatten} +\alias{list_flatten} +\title{Flatten a list} +\usage{ +list_flatten( + x, + name_spec = "{outer}_{inner}", + name_repair = c("minimal", "unique", "check_unique", "universal") +) +} +\arguments{ +\item{x}{A list.} + +\item{name_spec}{If both inner and outer names are present, control +how they are combined. Should be a glue specification that uses +variables \code{inner} and \code{outer}.} + +\item{name_repair}{One of \code{"minimal"}, \code{"unique"}, \code{"universal"}, or +\code{"check_unique"}. See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of these +options.} +} +\value{ +A list. The list might be shorter if \code{x} contains empty lists, +the same length if it contains lists of length 1 or no sub-lists, +or longer if it contains lists of length > 1. +} +\description{ +Flattening a list removes a single layer of internal hierarchy, +i.e. it inlines elements that are lists leaving non-lists alone. +} +\examples{ +x <- list(1, list(2, 3), list(4, list(5))) +x \%>\% list_flatten() \%>\% str() +x \%>\% list_flatten() \%>\% list_flatten() \%>\% str() + +# Flat lists are left as is +list(1, 2, 3, 4, 5) \%>\% list_flatten() \%>\% str() + +# Empty lists will disappear +list(1, list(), 2, list(3)) \%>\% list_flatten() \%>\% str() + +# Another way to see this is that it reduces the depth of the list +x <- list( + list(), + list(list()) +) +x \%>\% pluck_depth() +x \%>\% list_flatten() \%>\% pluck_depth() + +# Use name_spec to control how inner and outer names are combined +x <- list(x = list(a = 1, b = 2), y = list(c = 1, d = 2)) +x \%>\% list_flatten() \%>\% names() +x \%>\% list_flatten(name_spec = "{outer}") \%>\% names() +x \%>\% list_flatten(name_spec = "{inner}") \%>\% names() +} diff --git a/man/lmap.Rd b/man/lmap.Rd index 20b51299..21d08167 100644 --- a/man/lmap.Rd +++ b/man/lmap.Rd @@ -23,15 +23,12 @@ length.)} Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} -\item{.p}{A predicate function (i.e. a function that returns either \code{TRUE} -or \code{FALSE}) specified in one of the following ways: -\itemize{ -\item A named function, e.g. \code{is.character}. -\item An anonymous function, e.g. \verb{\\(x) all(x < 0)} or \code{function(x) all(x < 0)}. -\item A formula, e.g. \code{~ all(.x < 0)}. You must use \code{.x} to refer to the first -argument). Only recommended if you require backward compatibility with -older versions of R. -}} +\item{.p}{A single predicate function, a formula describing such a +predicate function, or a logical vector of the same length as \code{.x}. +Alternatively, if the elements of \code{.x} are themselves lists of +objects, a string indicating the name of a logical element in the +inner lists. Only those elements where \code{.p} evaluates to +\code{TRUE} will be modified.} \item{.else}{A function applied to elements of \code{.x} for which \code{.p} returns \code{FALSE}.} @@ -45,7 +42,8 @@ installed, you can use \code{vars()} and tidyselect helpers to select elements.} } \value{ -A list. There are no guarantees about the length. +A list or data frame, matching \code{.x}. There are no guarantees about +the length. } \description{ \code{lmap()}, \code{lmap_at()} and \code{lmap_if()} are similar to \code{map()}, \code{map_at()} and diff --git a/man/map.Rd b/man/map.Rd index db9d4768..85f26cca 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -6,11 +6,8 @@ \alias{map_chr} \alias{map_int} \alias{map_dbl} -\alias{map_dfr} -\alias{map_df} -\alias{map_dfc} \alias{walk} -\title{Apply a function to each element of a list or atomic vector} +\title{Apply a function to each element of a vector} \usage{ map(.x, .f, ...) @@ -22,10 +19,6 @@ map_int(.x, .f, ...) map_dbl(.x, .f, ...) -map_dfr(.x, .f, ..., .id = NULL) - -map_dfc(.x, .f, ...) - walk(.x, .f, ...) } \arguments{ @@ -48,48 +41,28 @@ set a default value if the indexed element is \code{NULL} or does not exist. Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} - -\item{.id}{Either a string or \code{NULL}. If a string, the output will contain -a variable with that name, storing either the name (if \code{.x} is named) or -the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no -variable will be created. - -Only applies to \verb{_dfr} variant.} } \value{ +The output length is determined by the length of the input. The output type is determined by the suffix: \itemize{ -\item No suffix: returns a list the same length as the input. It will be -named if the input was named. +\item No suffix: a list. \item \verb{_lgl}, \verb{_int}, \verb{_dbl}, \verb{_chr} return a logical, integer, double, or character vector respectively. It will be named if the input was named. -\item \verb{_dfc} and \verb{_dfr()} all return a data frame created by row-binding and -column-binding respectively. They require dplyr to be installed. -} - -\itemize{ \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to -use in pipe. +use in a pipe. } } \description{ The map functions transform their input by applying a function to -each element of a list or atomic vector and returning an object of the same length as the input. +each element of a list or atomic vector and returning an object of +the same length as the input. \itemize{ \item \code{map()} always returns a list. See the \code{\link[=modify]{modify()}} family for versions that return an object of the same type as the input. \item \code{map_lgl()}, \code{map_int()}, \code{map_dbl()} and \code{map_chr()} return an -atomic vector of the indicated type (or die trying). -\item \code{map_dfr()} and \code{map_dfc()} return a data frame created by -row-binding and column-binding respectively. They require dplyr -to be installed. \code{map_df()} is an alias for \code{map_dfr()}. -\item The returned values of \code{.f} must be of length one for each element -of \code{.x}. If \code{.f} uses an extractor function shortcut, \code{.default} -can be specified to handle values that are absent or empty. See -\code{\link[=as_mapper]{as_mapper()}} for more on \code{.default}. -} - -\itemize{ +atomic vector of the indicated type (or die trying). For these functions, +\code{.f} must return a length-1 vector of the appropriate type. \item \code{walk()} calls \code{.f} for its side-effect and returns the input \code{.x}. } @@ -151,15 +124,6 @@ mtcars \%>\% map(~ lm(mpg ~ wt, data = .x)) \%>\% map(summary) \%>\% map_dbl("r.squared") - -# If each element of the output is a data frame, use -# map_dfr to row-bind them together: -mtcars \%>\% - split(.$cyl) \%>\% - map(~ lm(mpg ~ wt, data = .x)) \%>\% - map_dfr(~ as.data.frame(t(as.matrix(coef(.))))) -# (if you also want to preserve the variable names see -# the broom package) } \seealso{ \code{\link[=map_if]{map_if()}} for applying a function to only those elements diff --git a/man/map2.Rd b/man/map2.Rd index c0228bbd..bced128f 100644 --- a/man/map2.Rd +++ b/man/map2.Rd @@ -6,9 +6,6 @@ \alias{map2_int} \alias{map2_dbl} \alias{map2_chr} -\alias{map2_dfr} -\alias{map2_dfc} -\alias{map2_df} \alias{walk2} \title{Map over two inputs} \usage{ @@ -22,10 +19,6 @@ map2_dbl(.x, .y, .f, ...) map2_chr(.x, .y, .f, ...) -map2_dfr(.x, .y, .f, ..., .id = NULL) - -map2_dfc(.x, .y, .f, ...) - walk2(.x, .y, .f, ...) } \arguments{ @@ -46,28 +39,16 @@ of R. Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} - -\item{.id}{Either a string or \code{NULL}. If a string, the output will contain -a variable with that name, storing either the name (if \code{.x} is named) or -the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no -variable will be created. - -Only applies to \verb{_dfr} variant.} } \value{ +The output length is determined by the length of the input. The output type is determined by the suffix: \itemize{ -\item No suffix: returns a list the same length as the input. It will be -named if the input was named. +\item No suffix: a list. \item \verb{_lgl}, \verb{_int}, \verb{_dbl}, \verb{_chr} return a logical, integer, double, or character vector respectively. It will be named if the input was named. -\item \verb{_dfc} and \verb{_dfr()} all return a data frame created by row-binding and -column-binding respectively. They require dplyr to be installed. -} - -\itemize{ \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to -use in pipe. +use in a pipe. } } \description{ diff --git a/man/map_dfr.Rd b/man/map_dfr.Rd new file mode 100644 index 00000000..19d764b5 --- /dev/null +++ b/man/map_dfr.Rd @@ -0,0 +1,89 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/map-df.R +\name{map_dfr} +\alias{map_dfr} +\alias{map_df} +\alias{map_dfc} +\alias{imap_dfr} +\alias{imap_dfc} +\alias{map2_dfr} +\alias{map2_dfc} +\alias{map2_df} +\alias{pmap_dfr} +\alias{pmap_dfc} +\alias{pmap_df} +\title{Functions that return data frames} +\usage{ +map_dfr(.x, .f, ..., .id = NULL) + +map_dfc(.x, .f, ...) + +imap_dfr(.x, .f, ..., .id = NULL) + +imap_dfc(.x, .f, ...) + +map2_dfr(.x, .y, .f, ..., .id = NULL) + +map2_dfc(.x, .y, .f, ...) + +pmap_dfr(.l, .f, ..., .id = NULL) + +pmap_dfc(.l, .f, ...) +} +\arguments{ +\item{.id}{Either a string or \code{NULL}. If a string, the output will contain +a variable with that name, storing either the name (if \code{.x} is named) or +the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no +variable will be created. + +Only applies to \verb{_dfr} variant.} +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} + +These variants of \code{\link[=map]{map()}}, \code{\link[=map2]{map2()}}, \code{\link[=imap]{imap()}}, and \code{\link[=pmap]{pmap()}} return data +frames. They have been deprecated because they use \code{dplyr::bind_rows()} +and \code{dplyr::bind_cols()} which often have confusing semantics, and their +names are suboptimal because they suggest they work like \verb{_lgl()}, \verb{_int()}, +and friends which require length 1 outputs, but actually they return results +of any size because the results are combined together without any size checks. + +Instead, we now recommend usin \code{map()}, \code{map2()}, etc with \code{\link[=list_rbind]{list_rbind()}} +and \code{\link[=list_cbind]{list_cbind()}}. These use \code{\link[vctrs:vec_bind]{vctrs::vec_rbind()}} and \code{\link[vctrs:vec_bind]{vctrs::vec_cbind()}} +under the hood, and have names that more clearly reflect their semantics. +} +\examples{ +# map --------------------------------------------- +# Was: +mtcars \%>\% + split(.$cyl) \%>\% + map(~ lm(mpg ~ wt, data = .x)) \%>\% + map_dfr(~ as.data.frame(t(as.matrix(coef(.))))) + +# Now: +mtcars \%>\% + split(.$cyl) \%>\% + map(~ lm(mpg ~ wt, data = .x)) \%>\% + map(~ as.data.frame(t(as.matrix(coef(.))))) \%>\% + list_rbind() + +# map2 --------------------------------------------- + +ex_fun <- function(arg1, arg2){ + col <- arg1 + arg2 + x <- as.data.frame(col) +} +arg1 <- 1:4 +arg2 <- 10:13 + +# was +map2_dfr(arg1, arg2, ex_fun) +# now +map2(arg1, arg2, ex_fun) \%>\% list_rbind() + +# was +map2_dfc(arg1, arg2, ex_fun) +# now +map2(arg1, arg2, ex_fun) \%>\% list_cbind() +} +\keyword{internal} diff --git a/man/map_if.Rd b/man/map_if.Rd index 6f83e765..7ed55406 100644 --- a/man/map_if.Rd +++ b/man/map_if.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/map.R +% Please edit documentation in R/map-if-at.R \name{map_if} \alias{map_if} \alias{map_at} @@ -15,15 +15,12 @@ map_depth(.x, .depth, .f, ..., .ragged = FALSE) \arguments{ \item{.x}{A list or atomic vector.} -\item{.p}{A predicate function (i.e. a function that returns either \code{TRUE} -or \code{FALSE}) specified in one of the following ways: -\itemize{ -\item A named function, e.g. \code{is.character}. -\item An anonymous function, e.g. \verb{\\(x) all(x < 0)} or \code{function(x) all(x < 0)}. -\item A formula, e.g. \code{~ all(.x < 0)}. You must use \code{.x} to refer to the first -argument). Only recommended if you require backward compatibility with -older versions of R. -}} +\item{.p}{A single predicate function, a formula describing such a +predicate function, or a logical vector of the same length as \code{.x}. +Alternatively, if the elements of \code{.x} are themselves lists of +objects, a string indicating the name of a logical element in the +inner lists. Only those elements where \code{.p} evaluates to +\code{TRUE} will be modified.} \item{.f}{A function, specified in one of the following ways: \itemize{ diff --git a/man/map_raw.Rd b/man/map_raw.Rd index 711b901f..99e2c7aa 100644 --- a/man/map_raw.Rd +++ b/man/map_raw.Rd @@ -6,7 +6,7 @@ \alias{imap_raw} \alias{pmap_raw} \alias{flatten_raw} -\title{Function that return raw vectors} +\title{Functions that return raw vectors} \usage{ map_raw(.x, .f, ...) @@ -20,6 +20,7 @@ flatten_raw(.x) } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} + These variants of \code{\link[=map]{map()}}, \code{\link[=map2]{map2()}}, \code{\link[=imap]{imap()}}, \code{\link[=pmap]{pmap()}}, and \code{\link[=flatten]{flatten()}} return raw vectors. They have been deprecated because they are of limited use and you can now use \code{map_vec()} instead. diff --git a/man/modify.Rd b/man/modify.Rd index 05449200..78c00138 100644 --- a/man/modify.Rd +++ b/man/modify.Rd @@ -44,15 +44,12 @@ function.} Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} -\item{.p}{A predicate function (i.e. a function that returns either \code{TRUE} -or \code{FALSE}) specified in one of the following ways: -\itemize{ -\item A named function, e.g. \code{is.character}. -\item An anonymous function, e.g. \verb{\\(x) all(x < 0)} or \code{function(x) all(x < 0)}. -\item A formula, e.g. \code{~ all(.x < 0)}. You must use \code{.x} to refer to the first -argument). Only recommended if you require backward compatibility with -older versions of R. -}} +\item{.p}{A single predicate function, a formula describing such a +predicate function, or a logical vector of the same length as \code{.x}. +Alternatively, if the elements of \code{.x} are themselves lists of +objects, a string indicating the name of a logical element in the +inner lists. Only those elements where \code{.p} evaluates to +\code{TRUE} will be modified.} \item{.else}{A function applied to elements of \code{.x} for which \code{.p} returns \code{FALSE}.} diff --git a/man/pmap.Rd b/man/pmap.Rd index 667cbd95..58ed6ab8 100644 --- a/man/pmap.Rd +++ b/man/pmap.Rd @@ -6,9 +6,6 @@ \alias{pmap_int} \alias{pmap_dbl} \alias{pmap_chr} -\alias{pmap_dfr} -\alias{pmap_dfc} -\alias{pmap_df} \alias{pwalk} \title{Map over multiple input simultaneously (in "parallel")} \usage{ @@ -22,10 +19,6 @@ pmap_dbl(.l, .f, ...) pmap_chr(.l, .f, ...) -pmap_dfr(.l, .f, ..., .id = NULL) - -pmap_dfc(.l, .f, ...) - pwalk(.l, .f, ...) } \arguments{ @@ -52,28 +45,16 @@ you can only refer to arguments by position. Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} - -\item{.id}{Either a string or \code{NULL}. If a string, the output will contain -a variable with that name, storing either the name (if \code{.x} is named) or -the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no -variable will be created. - -Only applies to \verb{_dfr} variant.} } \value{ +The output length is determined by the length of the input. The output type is determined by the suffix: \itemize{ -\item No suffix: returns a list the same length as the input. It will be -named if the input was named. +\item No suffix: a list. \item \verb{_lgl}, \verb{_int}, \verb{_dbl}, \verb{_chr} return a logical, integer, double, or character vector respectively. It will be named if the input was named. -\item \verb{_dfc} and \verb{_dfr()} all return a data frame created by row-binding and -column-binding respectively. They require dplyr to be installed. -} - -\itemize{ \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to -use in pipe. +use in a pipe. } } \description{ @@ -130,18 +111,6 @@ df <- data.frame( pmin(df$x, df$y) map2_dbl(df$x, df$y, min) pmap_dbl(df, min) - -# If you want to bind the results of your function rowwise, use: -# map2_dfr() or pmap_dfr() -ex_fun <- function(arg1, arg2){ -col <- arg1 + arg2 -x <- as.data.frame(col) -} -arg1 <- 1:4 -arg2 <- 10:13 -map2_dfr(arg1, arg2, ex_fun) -# If instead you want to bind by columns, use map2_dfc() or pmap_dfc() -map2_dfc(arg1, arg2, ex_fun) } \seealso{ Other map variants: diff --git a/man/splice.Rd b/man/splice.Rd index 093d8959..cae22890 100644 --- a/man/splice.Rd +++ b/man/splice.Rd @@ -17,8 +17,9 @@ A list. This splices all arguments into a list. Non-list objects and lists with a S3 class are encapsulated in a list before concatenation. + We no longer believe that implicit/automatic splicing is a good idea; -instead use \verb{!!!} in conjunction with \code{rlang::list2()}. +instead use \code{rlang::list2()} + \verb{!!!} or \code{\link[=list_flatten]{list_flatten()}}. } \examples{ inputs <- list(arg1 = "a", arg2 = "b") diff --git a/tests/testthat/_snaps/flatten.md b/tests/testthat/_snaps/flatten.md new file mode 100644 index 00000000..2dc7b8dd --- /dev/null +++ b/tests/testthat/_snaps/flatten.md @@ -0,0 +1,51 @@ +# flatten functions are deprecated + + Code + . <- flatten(list()) + Condition + Warning: + `flatten()` was deprecated in purrr 0.4.0. + Please use `list_flatten()` instead. + Code + . <- flatten_lgl(list()) + Condition + Warning: + `flatten_lgl()` was deprecated in purrr 0.4.0. + Please use `list_c()` instead. + Code + . <- flatten_int(list()) + Condition + Warning: + `flatten_lgl()` was deprecated in purrr 0.4.0. + Please use `list_c()` instead. + Code + . <- flatten_dbl(list()) + Condition + Warning: + `flatten_lgl()` was deprecated in purrr 0.4.0. + Please use `list_c()` instead. + Code + . <- flatten_chr(list()) + Condition + Warning: + `flatten_lgl()` was deprecated in purrr 0.4.0. + Please use `list_c()` instead. + Code + . <- flatten_dfr(list()) + Condition + Warning: + `flatten_dfr()` was deprecated in purrr 0.4.0. + Please use `list_rbind()` instead. + Code + . <- flatten_dfc(list()) + Condition + Warning: + `flatten_dfc()` was deprecated in purrr 0.4.0. + Please use `list_cbind()` instead. + Code + . <- flatten_df(list()) + Condition + Warning: + `flatten_df()` was deprecated in purrr 0.4.0. + Please use `list_rbind()` instead. + diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md new file mode 100644 index 00000000..97ea38a8 --- /dev/null +++ b/tests/testthat/_snaps/list-combine.md @@ -0,0 +1,98 @@ +# list_c() concatenates vctrs of compatible types + + Code + list_c(list("a", 1)) + Condition + Error: + ! Can't combine `..1` and `..2` . + +# list_c() can enforce ptype + + Code + list_c(list("a"), ptype = integer()) + Condition + Error: + ! Can't convert to . + +# list_cbind() column-binds compatible data frames + + Code + list_cbind(list(df1, df3)) + Condition + Error in `list_cbind()`: + ! Can't recycle `..1` (size 2) to match `..2` (size 3). + +# list_cbind() can enforce size + + Code + list_cbind(list(df1), size = 3) + Condition + Error: + ! Can't recycle input of size 2 to size 3. + +# list_rbind() row-binds compatible data.frames + + Code + list_rbind(list(df1, df3)) + Condition + Error in `list_rbind()`: + ! Can't combine `..1$x` and `..2$x` . + +# list_rbind() can enforce ptype + + Code + ptype <- data.frame(x = character(), stringsAsFactors = FALSE) + list_rbind(list(df1), ptype = ptype) + Condition + Error in `list_rbind()`: + ! Can't convert `..1$x` to match type of `x` . + +# assert input is a list + + Code + list_c(1) + Condition + Error in `list_c()`: + ! `x` must be a list, not a number. + Code + list_rbind(1) + Condition + Error in `list_rbind()`: + ! `x` must be a list, not a number. + Code + list_cbind(1) + Condition + Error in `list_cbind()`: + ! `x` must be a list, not a number. + +--- + + Code + list_c(mtcars) + Condition + Error in `list_c()`: + ! `x` must be a list, not a object. + Code + list_rbind(mtcars) + Condition + Error in `list_rbind()`: + ! `x` must be a list, not a object. + Code + list_cbind(mtcars) + Condition + Error in `list_cbind()`: + ! `x` must be a list, not a object. + +# assert input is list of data frames + + Code + list_rbind(list(1, mtcars, 3)) + Condition + Error in `list_rbind()`: + ! All elements of `x` must be data frames. Elements 1 and 3 are not. + Code + list_cbind(list(1, mtcars, 3)) + Condition + Error in `list_cbind()`: + ! All elements of `x` must be data frames. Elements 1 and 3 are not. + diff --git a/tests/testthat/_snaps/list-flatten.md b/tests/testthat/_snaps/list-flatten.md new file mode 100644 index 00000000..4cc08ba0 --- /dev/null +++ b/tests/testthat/_snaps/list-flatten.md @@ -0,0 +1,8 @@ +# requires a list + + Code + list_flatten(1:2) + Condition + Error in `list_flatten()`: + ! `x` must be a list, not an integer vector. + diff --git a/tests/testthat/_snaps/map-df.md b/tests/testthat/_snaps/map-df.md new file mode 100644 index 00000000..85070c4a --- /dev/null +++ b/tests/testthat/_snaps/map-df.md @@ -0,0 +1,69 @@ +# _df/_dfc/_dfr are deprecated + + Code + . <- map_df(list(), identity) + Condition + Warning: + `map_df()` was deprecated in purrr 0.4.0. + Please use `map()` + `list_rbind()` instead. + Code + . <- map_dfr(list(), identity) + Condition + Warning: + `map_dfr()` was deprecated in purrr 0.4.0. + Please use `map()` + `list_rbind()` instead. + Code + . <- map_dfc(list(), identity) + Condition + Warning: + `map_dfc()` was deprecated in purrr 0.4.0. + Please use `map()` + `list_cbind()` instead. + Code + . <- map2_df(list(), list(), identity) + Condition + Warning: + `map2_df()` was deprecated in purrr 0.4.0. + Please use `map2()` + `list_rbind()` instead. + Code + . <- map2_dfr(list(), list(), identity) + Condition + Warning: + `map2_dfr()` was deprecated in purrr 0.4.0. + Please use `map2()` + `list_rbind()` instead. + Code + . <- map2_dfc(list(), list(), identity) + Condition + Warning: + `map2_dfc()` was deprecated in purrr 0.4.0. + Please use `map2()` + `list_cbind()` instead. + Code + . <- imap_dfr(list(), identity) + Condition + Warning: + `imap_dfr()` was deprecated in purrr 0.4.0. + Please use `imap()` + `list_rbind()` instead. + Code + . <- imap_dfc(list(), identity) + Condition + Warning: + `imap_dfc()` was deprecated in purrr 0.4.0. + Please use `imap()` + `list_cbind()` instead. + Code + . <- pmap_df(list(), identity) + Condition + Warning: + `pmap_df()` was deprecated in purrr 0.4.0. + Please use `pmap()` + `list_rbind()` instead. + Code + . <- pmap_dfr(list(), identity) + Condition + Warning: + `pmap_dfr()` was deprecated in purrr 0.4.0. + Please use `pmap()` + `list_rbind()` instead. + Code + . <- pmap_dfc(list(), identity) + Condition + Warning: + `pmap_dfc()` was deprecated in purrr 0.4.0. + Please use `pmap()` + `list_cbind()` instead. + diff --git a/tests/testthat/_snaps/splice.md b/tests/testthat/_snaps/splice.md index a9877e0b..6daf6364 100644 --- a/tests/testthat/_snaps/splice.md +++ b/tests/testthat/_snaps/splice.md @@ -5,4 +5,5 @@ Condition Warning: `splice()` was deprecated in purrr 0.4.0. + Please use `list_flatten()` instead. diff --git a/tests/testthat/test-flatten.R b/tests/testthat/test-flatten.R index 6768ce2e..49be90d8 100644 --- a/tests/testthat/test-flatten.R +++ b/tests/testthat/test-flatten.R @@ -1,19 +1,40 @@ +test_that("flatten functions are deprecated", { + expect_snapshot({ + . <- flatten(list()) + . <- flatten_lgl(list()) + . <- flatten_int(list()) + . <- flatten_dbl(list()) + . <- flatten_chr(list()) + . <- flatten_dfr(list()) + . <- flatten_dfc(list()) + . <- flatten_df(list()) + }) +}) + test_that("input must be a list", { + local_options(lifecycle_verbosity = "quiet") + expect_bad_type_error(flatten(1), "`.x` must be a list, not a double vector") expect_bad_type_error(flatten_dbl(1), "`.x` must be a list, not a double vector") }) test_that("contents of list must be supported types", { + local_options(lifecycle_verbosity = "quiet") + expect_bad_element_type_error(flatten(list(quote(a))), "Element 1 of `.x` must be a vector, not a symbol") expect_bad_element_type_error(flatten(list(expression(a))), "Element 1 of `.x` must be a vector, not an expression vector") }) test_that("each second level element becomes first level element", { + local_options(lifecycle_verbosity = "quiet") + expect_equal(flatten(list(1:2)), list(1, 2)) expect_equal(flatten(list(1, 2)), list(1, 2)) }) test_that("can flatten all atomic vectors", { + local_options(lifecycle_verbosity = "quiet") + expect_equal(flatten(list(F)), list(F)) expect_equal(flatten(list(1L)), list(1L)) expect_equal(flatten(list(1)), list(1)) @@ -23,17 +44,23 @@ test_that("can flatten all atomic vectors", { }) test_that("NULLs are silently dropped", { + local_options(lifecycle_verbosity = "quiet") + expect_equal(flatten(list(NULL, NULL)), list()) expect_equal(flatten(list(NULL, 1)), list(1)) expect_equal(flatten(list(1, NULL)), list(1)) }) test_that("names are preserved", { + local_options(lifecycle_verbosity = "quiet") + expect_equal(flatten(list(list(x = 1), list(y = 1))), list(x = 1, y = 1)) expect_equal(flatten(list(list(a = 1, b = 2), 3)), list(a = 1, b = 2, 3)) }) test_that("names of 'scalar' elements are preserved", { + local_options(lifecycle_verbosity = "quiet") + out <- flatten(list(a = list(1), b = list(2))) expect_equal(out, list(a = 1, b = 2)) @@ -45,6 +72,8 @@ test_that("names of 'scalar' elements are preserved", { }) test_that("child names beat parent names", { + local_options(lifecycle_verbosity = "quiet") + out <- flatten(list(a = list(x = 1), b = list(y = 2))) expect_equal(out, list(x = 1, y = 2)) }) @@ -53,10 +82,14 @@ test_that("child names beat parent names", { # atomic flatten ---------------------------------------------------------- test_that("must be a list", { + local_options(lifecycle_verbosity = "quiet") + expect_bad_type_error(flatten_lgl(1), "must be a list") }) test_that("can flatten all atomic vectors", { + local_options(lifecycle_verbosity = "quiet") + expect_equal(flatten_lgl(list(F)), F) expect_equal(flatten_int(list(1L)), 1L) expect_equal(flatten_dbl(list(1)), 1) @@ -64,6 +97,8 @@ test_that("can flatten all atomic vectors", { }) test_that("preserves inner names", { + local_options(lifecycle_verbosity = "quiet") + expect_equal( flatten_dbl(list(c(a = 1), c(b = 2))), c(a = 1, b = 2) @@ -74,6 +109,7 @@ test_that("preserves inner names", { # data frame flatten ------------------------------------------------------ test_that("can flatten to a data frame with named lists", { + local_options(lifecycle_verbosity = "quiet") skip_if_not_installed("dplyr") dfs <- list(c(a = 1), c(b = 2)) diff --git a/tests/testthat/test-imap.R b/tests/testthat/test-imap.R index 65a39076..79314d7e 100644 --- a/tests/testthat/test-imap.R +++ b/tests/testthat/test-imap.R @@ -15,11 +15,6 @@ test_that("atomic vector imap works", { expect_equal(imap_dbl(x, ~ .x + as.numeric(.y)), x * 2) }) -test_that("data frame imap works", { - skip_if_not_installed("dplyr") - expect_identical(imap_dfc(x, paste), imap_dfr(x, paste)) -}) - test_that("iwalk returns invisibly", { expect_output(iwalk(mtcars, ~ cat(.y, ": ", median(.x), "\n", sep = ""))) }) diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R new file mode 100644 index 00000000..6b80c8a5 --- /dev/null +++ b/tests/testthat/test-list-combine.R @@ -0,0 +1,75 @@ +test_that("list_c() concatenates vctrs of compatible types", { + expect_identical(list_c(list(1L, 2:3)), c(1L, 2L, 3L)) + expect_identical(list_c(list(1, 2:3)), c(1, 2, 3)) + + expect_snapshot(error = TRUE, + list_c(list("a", 1)) + ) +}) + +test_that("list_c() can enforce ptype", { + expect_snapshot(error = TRUE, + list_c(list("a"), ptype = integer()) + ) +}) + +test_that("list_cbind() column-binds compatible data frames",{ + df1 <- data.frame(x = 1:2) + df2 <- data.frame(y = 1:2) + df3 <- data.frame(z = 1:3) + + expect_equal(list_cbind(list(df1, df2)), data.frame(x = 1:2, y = 1:2)) + expect_snapshot(error = TRUE, { + list_cbind(list(df1, df3)) + }) +}) + +test_that("list_cbind() can enforce size", { + df1 <- data.frame(x = 1:2) + expect_snapshot(error = TRUE, { + list_cbind(list(df1), size = 3) + }) +}) + +test_that("list_rbind() row-binds compatible data.frames", { + df1 <- data.frame(x = 1) + df2 <- data.frame(x = 2, y = 1) + df3 <- data.frame(x = "a", stringsAsFactors = FALSE) + + expect_equal(list_rbind(list(df1, df2)), data.frame(x = 1:2, y = c(NA, 1))) + + expect_snapshot(error = TRUE, { + list_rbind(list(df1, df3)) + }) +}) + +test_that("list_rbind() can enforce ptype", { + df1 <- data.frame(x = 1) + + expect_snapshot(error = TRUE, { + ptype <- data.frame(x = character(), stringsAsFactors = FALSE) + list_rbind(list(df1), ptype = ptype) + }) +}) + +test_that("assert input is a list", { + expect_snapshot(error = TRUE, { + list_c(1) + list_rbind(1) + list_cbind(1) + }) + + # and not just built on a list + expect_snapshot(error = TRUE, { + list_c(mtcars) + list_rbind(mtcars) + list_cbind(mtcars) + }) +}) + +test_that("assert input is list of data frames", { + expect_snapshot(error = TRUE, { + list_rbind(list(1, mtcars, 3)) + list_cbind(list(1, mtcars, 3)) + }) +}) diff --git a/tests/testthat/test-list-flatten.R b/tests/testthat/test-list-flatten.R new file mode 100644 index 00000000..25aa4865 --- /dev/null +++ b/tests/testthat/test-list-flatten.R @@ -0,0 +1,24 @@ +test_that("flattening removes single layer of nesting", { + expect_equal(list_flatten(list(list(1), list(2))), list(1, 2)) + expect_equal(list_flatten(list(list(1), list(list(2)))), list(1, list(2))) + expect_equal(list_flatten(list(list(1), list(), list(2))), list(1, 2)) +}) + +test_that("flattening a flat list is idempotent", { + expect_equal(list_flatten(list(1, 2)), list(1, 2)) +}) + +test_that("uses either inner or outer names if only one present", { + expect_equal(list_flatten(list(x = list(1), list(y = 2))), list(x = 1, y = 2)) +}) + +test_that("can control names if both present", { + x <- list(a = list(x = 1)) + expect_equal(list_flatten(x), list(a_x = 1)) + expect_equal(list_flatten(x, name_spec = "{inner}"), list(x = 1)) + expect_equal(list_flatten(x, name_spec = "{outer}"), list(a = 1)) +}) + +test_that("requires a list", { + expect_snapshot(list_flatten(1:2), error = TRUE) +}) diff --git a/tests/testthat/test-map-df.R b/tests/testthat/test-map-df.R new file mode 100644 index 00000000..ae0cfec8 --- /dev/null +++ b/tests/testthat/test-map-df.R @@ -0,0 +1,51 @@ +test_that("_df/_dfc/_dfr are deprecated", { + expect_snapshot({ + . <- map_df(list(), identity) + . <- map_dfr(list(), identity) + . <- map_dfc(list(), identity) + + . <- map2_df(list(), list(), identity) + . <- map2_dfr(list(), list(), identity) + . <- map2_dfc(list(), list(), identity) + + . <- imap_dfr(list(), identity) + . <- imap_dfc(list(), identity) + + . <- pmap_df(list(), identity) + . <- pmap_dfr(list(), identity) + . <- pmap_dfc(list(), identity) + }) +}) + +test_that("row and column binding work", { + local_options(lifecycle_verbosity = "quiet") + + skip_if_not_installed("dplyr") + local_name_repair_quiet() + + mtcar_mod <- mtcars %>% + split(.$cyl) %>% + map(~ lm(mpg ~ wt, data = .x)) + + f_coef <- function(x) as.data.frame(t(as.matrix(coef(x)))) + expect_length(mtcar_mod %>% map_dfr(f_coef), 2) + expect_length(mtcar_mod %>% map_dfc(f_coef), 6) +}) + +test_that("data frame imap works", { + local_options(lifecycle_verbosity = "quiet") + skip_if_not_installed("dplyr") + x <- set_names(1:3) + expect_identical(imap_dfc(x, paste), imap_dfr(x, paste)) +}) + +test_that("outputs are suffixes have correct type for data frames", { + local_options(lifecycle_verbosity = "quiet") + skip_if_not_installed("dplyr") + local_name_repair_quiet() + + local_options(rlang_message_verbosity = "quiet") + x <- 1:3 + expect_s3_class(pmap_dfr(list(x), as.data.frame), "data.frame") + expect_s3_class(pmap_dfc(list(x), as.data.frame), "data.frame") +}) diff --git a/tests/testthat/test-map-if-at.R b/tests/testthat/test-map-if-at.R new file mode 100644 index 00000000..50090dbf --- /dev/null +++ b/tests/testthat/test-map-if-at.R @@ -0,0 +1,64 @@ +test_that("map_if() and map_at() always return a list", { + skip_if_not_installed("tibble") + df <- tibble::tibble(x = 1, y = "a") + expect_identical(map_if(df, is.character, ~"out"), list(x = 1, y = "out")) + expect_identical(map_at(df, 1, ~"out"), list(x = "out", y = "a")) +}) + +test_that("map_at() works with tidyselect", { + skip_if_not_installed("tidyselect") + local_options(lifecycle_verbosity = "quiet") + + x <- list(a = "b", b = "c", aa = "bb") + one <- map_at(x, vars(a), toupper) + expect_identical(one$a, "B") + expect_identical(one$aa, "bb") + two <- map_at(x, vars(tidyselect::contains("a")), toupper) + expect_identical(two$a, "B") + expect_identical(two$aa, "BB") +}) + +test_that("negative .at omits locations", { + x <- c(1, 2, 3) + out <- map_at(x, -1, ~ .x * 2) + expect_equal(out, list(1, 4, 6)) +}) + +test_that("map_if requires predicate functions", { + expect_error(map_if(1:3, ~ NA, ~ "foo"), ", not a missing value") +}) + +test_that("`.else` maps false elements", { + expect_identical(map_if(-1:1, ~ .x > 0, paste, .else = ~ "bar", "suffix"), list("bar", "bar", "1 suffix")) +}) + + +test_that("map_depth modifies values at specified depth", { + x1 <- list(list(list(1:3, 4:6))) + + expect_equal(map_depth(x1, 0, length), 1) + expect_equal(map_depth(x1, 1, length), list(1)) + expect_equal(map_depth(x1, 2, length), list(list(2))) + expect_equal(map_depth(x1, 3, length), list(list(list(3, 3)))) + expect_equal(map_depth(x1, -1, length), list(list(list(3, 3)))) + expect_equal(map_depth(x1, 4, length), list(list(list(list(1, 1, 1), list(1, 1, 1))))) + expect_error(map_depth(x1, 5, length), "List not deep enough") #FIXME + expect_error(map_depth(x1, 6, length), "List not deep enough") + expect_error(map_depth(x1, -5, length), "Invalid depth") +}) + +test_that("map_depth() with .ragged = TRUE operates on leaves", { + x1 <- list( + list(1), + list(list(2)) + ) + exp <- list( + list(list(2)), + list(list(3)) + ) + + expect_equal(map_depth(x1, 3, ~ . + 1, .ragged = TRUE), exp) + expect_equal(map_depth(x1, -1, ~ . + 1, .ragged = TRUE), exp) + # .ragged should be TRUE is .depth < 0 + expect_equal(map_depth(x1, -1, ~ . + 1), exp) +}) diff --git a/tests/testthat/test-map.R b/tests/testthat/test-map.R index 601ea551..9334997f 100644 --- a/tests/testthat/test-map.R +++ b/tests/testthat/test-map.R @@ -52,49 +52,10 @@ test_that("map forces arguments in same way as base R", { expect_equal(f_map[[2]](0), f_base[[2]](0)) }) -test_that("row and column binding work", { - skip_if_not_installed("dplyr") - local_name_repair_quiet() - - mtcar_mod <- mtcars %>% - split(.$cyl) %>% - map(~ lm(mpg ~ wt, data = .x)) - - f_coef <- function(x) as.data.frame(t(as.matrix(coef(x)))) - expect_length(mtcar_mod %>% map_dfr(f_coef), 2) - expect_length(mtcar_mod %>% map_dfc(f_coef), 6) -}) - test_that("walk is used for side-effects", { expect_output(walk(1:3, str)) }) -test_that("map_if() and map_at() always return a list", { - skip_if_not_installed("tibble") - df <- tibble::tibble(x = 1, y = "a") - expect_identical(map_if(df, is.character, ~"out"), list(x = 1, y = "out")) - expect_identical(map_at(df, 1, ~"out"), list(x = "out", y = "a")) -}) - -test_that("map_at() works with tidyselect", { - skip_if_not_installed("tidyselect") - local_options(lifecycle_verbosity = "quiet") - - x <- list(a = "b", b = "c", aa = "bb") - one <- map_at(x, vars(a), toupper) - expect_identical(one$a, "B") - expect_identical(one$aa, "bb") - two <- map_at(x, vars(tidyselect::contains("a")), toupper) - expect_identical(two$a, "B") - expect_identical(two$aa, "BB") -}) - -test_that("negative .at omits locations", { - x <- c(1, 2, 3) - out <- map_at(x, -1, ~ .x * 2) - expect_equal(out, list(1, 4, 6)) -}) - test_that("map works with calls and pairlists", { out <- map(quote(f(x)), ~ quote(z)) expect_equal(out, list(quote(z), quote(z))) @@ -111,43 +72,6 @@ test_that("primitive dispatch correctly", { expect_identical(map(list(x, x), as.character), list("dispatched!", "dispatched!")) }) -test_that("map_if requires predicate functions", { - expect_error(map_if(1:3, ~ NA, ~ "foo"), ", not a missing value") -}) - -test_that("`.else` maps false elements", { - expect_identical(map_if(-1:1, ~ .x > 0, paste, .else = ~ "bar", "suffix"), list("bar", "bar", "1 suffix")) -}) - -test_that("map_depth modifies values at specified depth", { - x1 <- list(list(list(1:3, 4:6))) - - expect_equal(map_depth(x1, 0, length), 1) - expect_equal(map_depth(x1, 1, length), list(1)) - expect_equal(map_depth(x1, 2, length), list(list(2))) - expect_equal(map_depth(x1, 3, length), list(list(list(3, 3)))) - expect_equal(map_depth(x1, -1, length), list(list(list(3, 3)))) - expect_equal(map_depth(x1, 4, length), list(list(list(list(1, 1, 1), list(1, 1, 1))))) - expect_error(map_depth(x1, 5, length), "List not deep enough") #FIXME - expect_error(map_depth(x1, 6, length), "List not deep enough") - expect_error(map_depth(x1, -5, length), "Invalid depth") -}) - -test_that("map_depth() with .ragged = TRUE operates on leaves", { - x1 <- list( - list(1), - list(list(2)) - ) - exp <- list( - list(list(2)), - list(list(3)) - ) - - expect_equal(map_depth(x1, 3, ~ . + 1, .ragged = TRUE), exp) - expect_equal(map_depth(x1, -1, ~ . + 1, .ragged = TRUE), exp) - # .ragged should be TRUE is .depth < 0 - expect_equal(map_depth(x1, -1, ~ . + 1), exp) -}) test_that("error message follows style guide when result is not length 1", { x <- list(list(a = 1L), list(a = 2:3)) diff --git a/tests/testthat/test-pmap.R b/tests/testthat/test-pmap.R index 671a52a0..be73e966 100644 --- a/tests/testthat/test-pmap.R +++ b/tests/testthat/test-pmap.R @@ -47,16 +47,6 @@ test_that("outputs are suffixes have correct type", { expect_bare(pmap_chr(list(x), paste), "character") }) -test_that("outputs are suffixes have correct type for data frames", { - skip_if_not_installed("dplyr") - local_name_repair_quiet() - - local_options(rlang_message_verbosity = "quiet") - x <- 1:3 - expect_s3_class(pmap_dfr(list(x), as.data.frame), "data.frame") - expect_s3_class(pmap_dfc(list(x), as.data.frame), "data.frame") -}) - test_that("pmap on data frames performs rowwise operations", { mtcars2 <- mtcars[c("mpg", "cyl")] expect_length(pmap(mtcars2, paste), nrow(mtcars))