From 078c80ed9f70caea31ccb365c49ae9d119950936 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 2 Sep 2022 15:01:53 -0500 Subject: [PATCH 01/27] Implement list_flatten()/_c()/_rbind()/_cbind() --- NAMESPACE | 4 ++++ R/list-combine.R | 54 +++++++++++++++++++++++++++++++++++++++++++++ R/list-flatten.R | 31 ++++++++++++++++++++++++++ _pkgdown.yml | 2 ++ man/list_c.Rd | 43 ++++++++++++++++++++++++++++++++++++ man/list_flatten.Rd | 34 ++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+) create mode 100644 R/list-combine.R create mode 100644 R/list-flatten.R create mode 100644 man/list_c.Rd create mode 100644 man/list_flatten.Rd 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/R/list-combine.R b/R/list-combine.R new file mode 100644 index 00000000..48e565ad --- /dev/null +++ b/R/list-combine.R @@ -0,0 +1,54 @@ +#' 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. +#' @param ptype An optional prototype to ensure that the output type is always +#' the same. +#' @param name_repair One of `"unique"`, `"universal"`, or `"check_unique"`. +#' See [vctrs::vec_as_names()] for the meaning of these options. +#' @export +#' @examples +#' x <- list(a = 1, b = 2, c = 3) +#' list_c(x) +#' list_rbind(x) +#' list_cbind(x) +list_c <- function(x, ptype = NULL) { + check_is_list(x) + vctrs::vec_unchop(x, ptype = ptype) +} + +#' @export +#' @rdname list_c +list_cbind <- function( + x, + name_repair = c("unique", "universal", "check_unique"), + ptype = NULL + ) { + check_is_list(x) + vctrs::vec_cbind(!!!x, .name_repair = name_repair, .ptype = ptype) +} + +#' @export +#' @rdname list_c +list_rbind <- function(x, id = NULL, ptype = NULL) { + check_is_list(x) + vctrs::vec_rbind(!!!x, .names_to = id, .ptype = ptype) +} + +check_is_list <- function(x, error_call = caller_env()) { + if (!vctrs::vec_is_list(x)) { + cli::cli_abort( + "{.arg x} must be a list, not {friendly_type_of(x)}", + call = error_call + ) + } +} diff --git a/R/list-flatten.R b/R/list-flatten.R new file mode 100644 index 00000000..f54f6a2c --- /dev/null +++ b/R/list-flatten.R @@ -0,0 +1,31 @@ +#' Flatten a list +#' +#' Flattening a list removes a single layer of internal hierarchy. +#' +#' @param x A list +#' @return A list, probably longer. +#' @export +#' @examples +#' x <- list(1, list(2, 3), list(4, list(5))) +#' x %>% list_flatten() %>% str() +#' x %>% list_flatten() %>% list_flatten() %>% str() +#' +#' # It's now as flat as it can get so further flattening leaves it +#' # changed. +#' x %>% list_flatten() %>% list_flatten() %>% 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() +list_flatten <- function(x) { + check_is_list(x) + + is_nested <- map_lgl(x, vctrs::vec_is_list) + x[!is_nested] <- map(x[!is_nested], list) + + unlist(x, recursive = FALSE) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index f54eac49..2fdbb7d1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -82,6 +82,8 @@ reference: contents: - accumulate - flatten + - list_c + - list_flatten - list_modify - reduce - transpose diff --git a/man/list_c.Rd b/man/list_c.Rd new file mode 100644 index 00000000..542b9504 --- /dev/null +++ b/man/list_c.Rd @@ -0,0 +1,43 @@ +% 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"), + ptype = NULL +) + +list_rbind(x, id = NULL, ptype = NULL) +} +\arguments{ +\item{x}{A list.} + +\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.} +} +\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{ +x <- list(a = 1, b = 2, c = 3) +list_c(x) +list_rbind(x) +list_cbind(x) +} diff --git a/man/list_flatten.Rd b/man/list_flatten.Rd new file mode 100644 index 00000000..41da99f0 --- /dev/null +++ b/man/list_flatten.Rd @@ -0,0 +1,34 @@ +% 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) +} +\arguments{ +\item{x}{A list} +} +\value{ +A list, probably longer. +} +\description{ +Flattening a list removes a single layer of internal hierarchy. +} +\examples{ +x <- list(1, list(2, 3), list(4, list(5))) +x \%>\% list_flatten() \%>\% str() +x \%>\% list_flatten() \%>\% list_flatten() \%>\% str() + +# It's now as flat as it can get so further flattening leaves it +# changed. +x \%>\% list_flatten() \%>\% list_flatten() \%>\% 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() +} From 50c74f37f13dcaa094b78c78d6f9ad4a7e7cbf1a Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 2 Sep 2022 16:13:20 -0500 Subject: [PATCH 02/27] Deprecate flatten_* --- R/arrays.R | 2 +- R/flatten.R | 46 +++++++++++++++++++++------- R/lmap.R | 9 ++++-- R/splice.R | 5 ++-- man/flatten.Rd | 31 ++++++++++++------- man/lmap.Rd | 3 +- tests/testthat/_snaps/flatten.md | 51 ++++++++++++++++++++++++++++++++ tests/testthat/_snaps/splice.md | 1 + tests/testthat/test-flatten.R | 36 ++++++++++++++++++++++ 9 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 tests/testthat/_snaps/flatten.md 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..1d55cbc2 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 @@ -17,40 +24,49 @@ #' @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 +74,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 +84,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 +94,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/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/splice.R b/R/splice.R index 87167aad..1a7d8afd 100644 --- a/R/splice.R +++ b/R/splice.R @@ -20,7 +20,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) } @@ -34,5 +34,6 @@ splice_if <- function(.x, .p) { out[unspliced] <- map2(out[unspliced], names(out)[unspliced], set_names) } - flatten(out) + # Avoid deprecation message by inlining flatten() + .Call(flatten_impl, out) } diff --git a/man/flatten.Rd b/man/flatten.Rd index eaf29626..8c708afe 100644 --- a/man/flatten.Rd +++ b/man/flatten.Rd @@ -47,18 +47,29 @@ 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() } diff --git a/man/lmap.Rd b/man/lmap.Rd index fd30d1cb..2ec8c48b 100644 --- a/man/lmap.Rd +++ b/man/lmap.Rd @@ -39,7 +39,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/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/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)) From 13cf2c3862c733d499127726e206940b9337b82c Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 2 Sep 2022 16:14:38 -0500 Subject: [PATCH 03/27] Add news bullets --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 5ab561c9..85b4c316 100644 --- a/NEWS.md +++ b/NEWS.md @@ -36,8 +36,14 @@ * `*_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()`. + ## 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. + * `as_mapper()` is now around twice as fast when used with character, integer, or list (#820). From aeba6bc7f32a56883e28163a528fc7fa6859608b Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 2 Sep 2022 16:30:13 -0500 Subject: [PATCH 04/27] Move _dfc() and _dfr() to own file --- R/imap.R | 14 ---- R/map-df.R | 142 +++++++++++++++++++++++++++++++++++ R/map-raw.R | 3 +- R/map.R | 48 +----------- R/map2.R | 22 ------ R/pmap.R | 37 --------- R/reduce.R | 2 +- man/accumulate.Rd | 2 +- man/flatten.Rd | 7 -- man/imap.Rd | 13 ---- man/map.Rd | 31 +------- man/map2.Rd | 17 +---- man/map_dfr.Rd | 88 ++++++++++++++++++++++ man/map_raw.Rd | 3 +- man/pmap.Rd | 29 +------ tests/testthat/test-imap.R | 5 -- tests/testthat/test-map-df.R | 28 +++++++ tests/testthat/test-map.R | 13 ---- tests/testthat/test-pmap.R | 10 --- 19 files changed, 272 insertions(+), 242 deletions(-) create mode 100644 R/map-df.R create mode 100644 man/map_dfr.Rd create mode 100644 tests/testthat/test-map-df.R diff --git a/R/imap.R b/R/imap.R index 02257aa7..ca69054b 100644 --- a/R/imap.R +++ b/R/imap.R @@ -48,20 +48,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/map-df.R b/R/map-df.R new file mode 100644 index 00000000..594d9c18 --- /dev/null +++ b/R/map-df.R @@ -0,0 +1,142 @@ +#' 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 have often 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 accept results +#' of anything because the results are combined to together. +#' +#' You can now instead use functions with `_rbind()` and `_cbind()` suffixes +#' which 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_rbind(~ as.data.frame(t(as.matrix(coef(.))))) +#' +#' # 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_rbind(arg1, arg2, ex_fun) +#' +#' # was +#' map2_dfc(arg1, arg2, ex_fun) +#' # now +#' map2_cbind(arg1, arg2, ex_fun) +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_dfr +#' @usage NULL +#' @export +map_df <- map_dfr + +#' @rdname map_dfr +#' @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_dfr +#' @export +imap_dfr <- function(.x, .f, ..., .id = NULL) { + .f <- as_mapper(.f, ...) + map2_dfr(.x, vec_index(.x), .f, ..., .id = .id) +} + +#' @rdname map_dfr +#' @export +imap_dfc <- function(.x, .f, ...) { + .f <- as_mapper(.f, ...) + map2_dfc(.x, vec_index(.x), .f, ...) +} + +#' @rdname map_dfr +#' @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 map_dfr +#' @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 map_dfr +#' @export +#' @usage NULL +map2_df <- map2_dfr + +#' @rdname map_dfr +#' @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 map_dfr +#' @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 map_dfr +#' @export +#' @usage NULL +pmap_df <- pmap_dfr 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 77160344..ff92b127 100644 --- a/R/map.R +++ b/R/map.R @@ -11,9 +11,8 @@ #' * `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()`. +#' * `map_rbind()` and `map_cbind()` return a data frame created by +#' row-binding and column-binding respectively. #' #' * The returned values of `.f` must be of length one for each element #' of `.x`. If `.f` uses an extractor function shortcut, `.default` @@ -34,8 +33,7 @@ #' automatically coerced upwards (i.e. logical -> integer -> double -> #' character). 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. +#' * `_rbind()` and `_cbind()` return a data frame. #' #' @export #' @family map variants @@ -98,15 +96,6 @@ #' 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") @@ -215,37 +204,6 @@ map_dbl <- function(.x, .f, ...) { } -#' @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`. diff --git a/R/map2.R b/R/map2.R index 98fd8008..d66fc036 100644 --- a/R/map2.R +++ b/R/map2.R @@ -54,29 +54,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 f762b6c2..56238a9b 100644 --- a/R/pmap.R +++ b/R/pmap.R @@ -64,18 +64,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)) { @@ -126,31 +114,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 26e92080..45c17559 100644 --- a/R/reduce.R +++ b/R/reduce.R @@ -448,7 +448,7 @@ seq_len2 <- function(start, end) { #' rerun(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_rbind(~ tibble(value = .x, step = 1:100), .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/accumulate.Rd b/man/accumulate.Rd index 621e14f6..fbbee94a 100644 --- a/man/accumulate.Rd +++ b/man/accumulate.Rd @@ -169,7 +169,7 @@ library(ggplot2) rerun(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_rbind(~ tibble(value = .x, step = 1:100), .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 8c708afe..92488a2e 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 diff --git a/man/imap.Rd b/man/imap.Rd index 9dd8a7a4..2c5f7f16 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{ @@ -54,13 +48,6 @@ by position and name at different levels. If a component is not present, the value of \code{.default} will be returned.} \item{...}{Additional arguments passed on to the mapped function.} - -\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/map.Rd b/man/map.Rd index 3c5599e9..9ff3f925 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -6,9 +6,6 @@ \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} \usage{ @@ -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{ @@ -55,13 +48,6 @@ by position and name at different levels. If a component is not present, the value of \code{.default} will be returned.} \item{...}{Additional arguments passed on to the mapped function.} - -\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 type is determined by the suffix: @@ -72,8 +58,7 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). 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. +\item \verb{_rbind()} and \verb{_cbind()} return a data frame. } \itemize{ @@ -89,9 +74,8 @@ each element of a list or atomic vector and returning an object of the same leng 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 \code{map_rbind()} and \code{map_cbind()} return a data frame created by +row-binding and column-binding respectively. \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 @@ -160,15 +144,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 57d98417..234ed3c9 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{ @@ -59,13 +52,6 @@ by position and name at different levels. If a component is not present, the value of \code{.default} will be returned.} \item{...}{Additional arguments passed on to the mapped function.} - -\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 type is determined by the suffix: @@ -76,8 +62,7 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). 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. +\item \verb{_rbind()} and \verb{_cbind()} return a data frame. } \itemize{ diff --git a/man/map_dfr.Rd b/man/map_dfr.Rd new file mode 100644 index 00000000..a66f29ef --- /dev/null +++ b/man/map_dfr.Rd @@ -0,0 +1,88 @@ +% 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 have often 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 accept results +of anything because the results are combined to together. + +You can now instead use functions with \verb{_rbind()} and \verb{_cbind()} suffixes +which use \code{vctrs::vec_rbind()} and \code{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_rbind(~ as.data.frame(t(as.matrix(coef(.))))) + +# 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_rbind(arg1, arg2, ex_fun) + +# was +map2_dfc(arg1, arg2, ex_fun) +# now +map2_cbind(arg1, arg2, ex_fun) +} +\keyword{internal} 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/pmap.Rd b/man/pmap.Rd index 066890be..1a4b22fc 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{ @@ -60,13 +53,6 @@ by position and name at different levels. If a component is not present, the value of \code{.default} will be returned.} \item{...}{Additional arguments passed on to the mapped function.} - -\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 type is determined by the suffix: @@ -77,8 +63,7 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). 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. +\item \verb{_rbind()} and \verb{_cbind()} return a data frame. } \itemize{ @@ -140,18 +125,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/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-map-df.R b/tests/testthat/test-map-df.R new file mode 100644 index 00000000..0668afb2 --- /dev/null +++ b/tests/testthat/test-map-df.R @@ -0,0 +1,28 @@ +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("data frame imap works", { + 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", { + 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.R b/tests/testthat/test-map.R index 9ff057bd..23c104e7 100644 --- a/tests/testthat/test-map.R +++ b/tests/testthat/test-map.R @@ -54,19 +54,6 @@ 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)) }) diff --git a/tests/testthat/test-pmap.R b/tests/testthat/test-pmap.R index dcb65ccc..73026796 100644 --- a/tests/testthat/test-pmap.R +++ b/tests/testthat/test-pmap.R @@ -46,16 +46,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)) From 87304a7c219704adfc4550333b83055a3440bd36 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sat, 3 Sep 2022 08:57:49 -0500 Subject: [PATCH 05/27] Polish list_flatten() implementation * Use `modify_if` * Use `vec_unchop()` * Test * Add `name_spec` argument * Simplify `splice_if` using `name_spec` --- R/list-flatten.R | 31 ++++++++++++++++++--------- R/splice.R | 9 +------- man/list_flatten.Rd | 26 ++++++++++++++++------ tests/testthat/_snaps/list-flatten.md | 8 +++++++ tests/testthat/test-list-flatten.R | 24 +++++++++++++++++++++ 5 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 tests/testthat/_snaps/list-flatten.md create mode 100644 tests/testthat/test-list-flatten.R diff --git a/R/list-flatten.R b/R/list-flatten.R index f54f6a2c..6dc93b6b 100644 --- a/R/list-flatten.R +++ b/R/list-flatten.R @@ -2,17 +2,24 @@ #' #' Flattening a list removes a single layer of internal hierarchy. #' -#' @param x A list -#' @return A list, probably longer. +#' @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`. +#' @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() #' -#' # It's now as flat as it can get so further flattening leaves it -#' # changed. -#' x %>% list_flatten() %>% 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( @@ -21,11 +28,15 @@ #' ) #' x %>% pluck_depth() #' x %>% list_flatten() %>% pluck_depth() -list_flatten <- function(x) { +#' +#' # 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}") { check_is_list(x) - is_nested <- map_lgl(x, vctrs::vec_is_list) - x[!is_nested] <- map(x[!is_nested], list) - - unlist(x, recursive = FALSE) + x <- modify_if(x, vctrs::vec_is_list, identity, .else = list) + vec_unchop(x, ptype = list(), name_spec = name_spec) } diff --git a/R/splice.R b/R/splice.R index 1a7d8afd..e247ce1b 100644 --- a/R/splice.R +++ b/R/splice.R @@ -28,12 +28,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) - } - - # Avoid deprecation message by inlining flatten() - .Call(flatten_impl, out) + list_flatten(out, name_spec = "{inner}") } diff --git a/man/list_flatten.Rd b/man/list_flatten.Rd index 41da99f0..5fb5197d 100644 --- a/man/list_flatten.Rd +++ b/man/list_flatten.Rd @@ -4,13 +4,19 @@ \alias{list_flatten} \title{Flatten a list} \usage{ -list_flatten(x) +list_flatten(x, name_spec = "{outer}_{inner}") } \arguments{ -\item{x}{A list} +\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}.} } \value{ -A list, probably longer. +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. @@ -20,9 +26,11 @@ x <- list(1, list(2, 3), list(4, list(5))) x \%>\% list_flatten() \%>\% str() x \%>\% list_flatten() \%>\% list_flatten() \%>\% str() -# It's now as flat as it can get so further flattening leaves it -# changed. -x \%>\% list_flatten() \%>\% 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( @@ -31,4 +39,10 @@ x <- 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/tests/testthat/_snaps/list-flatten.md b/tests/testthat/_snaps/list-flatten.md new file mode 100644 index 00000000..001c26cc --- /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/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) +}) From c12664453cb6ba408467cf609f03addf159e123d Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sat, 3 Sep 2022 08:59:05 -0500 Subject: [PATCH 06/27] Update connection to splice --- NEWS.md | 3 ++- R/splice.R | 3 ++- man/splice.Rd | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 85b4c316..3bd54a72 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. diff --git a/R/splice.R b/R/splice.R index e247ce1b..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. 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") From 90ce7e097ea9923b7f6168660bdf62a8e7e321f0 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sat, 3 Sep 2022 09:13:23 -0500 Subject: [PATCH 07/27] Test list_c() and friends --- R/list-combine.R | 19 +++++--- man/list_c.Rd | 18 ++++++-- tests/testthat/_snaps/list-combine.md | 66 +++++++++++++++++++++++++++ tests/testthat/test-list-combine.R | 59 ++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 tests/testthat/_snaps/list-combine.md create mode 100644 tests/testthat/test-list-combine.R diff --git a/R/list-combine.R b/R/list-combine.R index 48e565ad..0b167953 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -13,14 +13,21 @@ #' @param x A list. #' @param ptype An optional prototype to ensure that the output type is always #' the same. +#' @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 -#' x <- list(a = 1, b = 2, c = 3) -#' list_c(x) -#' list_rbind(x) -#' list_cbind(x) +#' x1 <- list(a = 1, b = 2, c = 3) +#' list_c(x1) +#' +#' x2 <- list( +#' data.frame(x = 1:2), +#' data.frame(y = "a") +#' ) +#' list_rbind(x2) +#' list_cbind(x2) list_c <- function(x, ptype = NULL) { check_is_list(x) vctrs::vec_unchop(x, ptype = ptype) @@ -31,10 +38,10 @@ list_c <- function(x, ptype = NULL) { list_cbind <- function( x, name_repair = c("unique", "universal", "check_unique"), - ptype = NULL + size = NULL ) { check_is_list(x) - vctrs::vec_cbind(!!!x, .name_repair = name_repair, .ptype = ptype) + vctrs::vec_cbind(!!!x, .name_repair = name_repair, .size = size) } #' @export diff --git a/man/list_c.Rd b/man/list_c.Rd index 542b9504..bdf3b266 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -11,7 +11,7 @@ list_c(x, ptype = NULL) list_cbind( x, name_repair = c("unique", "universal", "check_unique"), - ptype = NULL + size = NULL ) list_rbind(x, id = NULL, ptype = NULL) @@ -24,6 +24,9 @@ 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).} } \description{ \itemize{ @@ -36,8 +39,13 @@ together with \code{\link[vctrs:vec_bind]{vctrs::vec_cbind()}}. } } \examples{ -x <- list(a = 1, b = 2, c = 3) -list_c(x) -list_rbind(x) -list_cbind(x) +x1 <- list(a = 1, b = 2, c = 3) +list_c(x1) + +x2 <- list( + data.frame(x = 1:2), + data.frame(y = "a") +) +list_rbind(x2) +list_cbind(x2) } diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md new file mode 100644 index 00000000..e91399db --- /dev/null +++ b/tests/testthat/_snaps/list-combine.md @@ -0,0 +1,66 @@ +# 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 `vctrs::vec_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 `vctrs::vec_rbind()`: + ! Can't combine `..1$x` and `..2$x` . + +# list_rbind() can enforce ptype + + Code + list_rbind(list(df1), ptype = data.frame(x = character())) + Condition + Error in `vctrs::vec_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 double vector + Code + list_rbind(1) + Condition + Error in `list_rbind()`: + ! `x` must be a list, not a double vector + Code + list_cbind(1) + Condition + Error in `list_cbind()`: + ! `x` must be a list, not a double vector + diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R new file mode 100644 index 00000000..d3aa81f4 --- /dev/null +++ b/tests/testthat/test-list-combine.R @@ -0,0 +1,59 @@ +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") + + 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, { + list_rbind(list(df1), ptype = data.frame(x = character())) + }) +}) + +test_that("assert input is a list", { + expect_snapshot(error = TRUE, { + list_c(1) + list_rbind(1) + list_cbind(1) + }) +}) From 3a5192f30a0490b33f7ca4e534d2562ec39fee03 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sat, 3 Sep 2022 09:46:01 -0500 Subject: [PATCH 08/27] Move map_at(), _if(), _depth() to own file --- R/map-if-at.R | 143 +++++++++++++++++++++++++++++++ R/map.R | 144 -------------------------------- man/map_if.Rd | 2 +- tests/testthat/test-map-if-at.R | 64 ++++++++++++++ tests/testthat/test-map.R | 63 -------------- 5 files changed, 208 insertions(+), 208 deletions(-) create mode 100644 R/map-if-at.R create mode 100644 tests/testthat/test-map-if-at.R 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.R b/R/map.R index ff92b127..f8a3136c 100644 --- a/R/map.R +++ b/R/map.R @@ -101,80 +101,6 @@ map <- function(.x, .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 -#' @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 #' @export map_lgl <- function(.x, .f, ...) { @@ -203,7 +129,6 @@ map_dbl <- function(.x, .f, ...) { .Call(map_impl, environment(), ".x", ".f", "double") } - #' @rdname map #' @description * `walk()` calls `.f` for its side-effect and returns #' the input `.x`. @@ -216,72 +141,3 @@ 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/man/map_if.Rd b/man/map_if.Rd index 9e47c54e..50a25309 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} 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 23c104e7..1a36b016 100644 --- a/tests/testthat/test-map.R +++ b/tests/testthat/test-map.R @@ -58,32 +58,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))) @@ -100,43 +74,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)) From bf2774735d6741fbb048043546b0b0db797a30f1 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sun, 4 Sep 2022 09:13:10 -0500 Subject: [PATCH 09/27] Tweak map docs --- R/map.R | 28 +++++++++++----------------- man/map.Rd | 25 +++++++++---------------- man/map2.Rd | 6 +----- man/pmap.Rd | 6 +----- 4 files changed, 22 insertions(+), 43 deletions(-) diff --git a/R/map.R b/R/map.R index f8a3136c..6ea108de 100644 --- a/R/map.R +++ b/R/map.R @@ -1,9 +1,9 @@ -#' 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. @@ -11,13 +11,13 @@ #' * `map_lgl()`, `map_int()`, `map_dbl()` and `map_chr()` return an #' atomic vector of the indicated type (or die trying). #' -#' * `map_rbind()` and `map_cbind()` return a data frame created by -#' row-binding and column-binding respectively. +#' * `walk()` calls `.f` for its side-effect and returns +#' the input `.x`. #' -#' * 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`. +#' 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`. #' #' @inheritParams as_mapper #' @param .x A list or atomic vector. @@ -33,8 +33,8 @@ #' automatically coerced upwards (i.e. logical -> integer -> double -> #' character). It will be named if the input was named. #' -#' * `_rbind()` and `_cbind()` return a data frame. -#' +#' * `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 @@ -130,12 +130,6 @@ map_dbl <- function(.x, .f, ...) { } #' @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, ...) diff --git a/man/map.Rd b/man/map.Rd index 9ff3f925..e6f04a70 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -7,7 +7,7 @@ \alias{map_int} \alias{map_dbl} \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, ...) @@ -58,34 +58,27 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). It will be named if the input was named. -\item \verb{_rbind()} and \verb{_cbind()} return a data frame. -} - -\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_rbind()} and \code{map_cbind()} return a data frame created by -row-binding and column-binding respectively. -\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{ \item \code{walk()} calls \code{.f} for its side-effect and returns the input \code{.x}. } + +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}. } \examples{ # Compute normal distributions from an atomic vector diff --git a/man/map2.Rd b/man/map2.Rd index 234ed3c9..68167567 100644 --- a/man/map2.Rd +++ b/man/map2.Rd @@ -62,12 +62,8 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). It will be named if the input was named. -\item \verb{_rbind()} and \verb{_cbind()} return a data frame. -} - -\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/pmap.Rd b/man/pmap.Rd index 1a4b22fc..1ac68e3e 100644 --- a/man/pmap.Rd +++ b/man/pmap.Rd @@ -63,12 +63,8 @@ named if the input was named. or character vector respectively. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> character). It will be named if the input was named. -\item \verb{_rbind()} and \verb{_cbind()} return a data frame. -} - -\itemize{ \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to -use in pipe. +use in a pipe. } } \description{ From 96dfd4d0ed1a73f3291d43a51de7a581fa9b0262 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sun, 4 Sep 2022 09:26:35 -0500 Subject: [PATCH 10/27] Implement map_c()/map_cbind()/map_rbind() --- NAMESPACE | 3 ++ R/map-combine.R | 32 ++++++++++++++++ R/map-df.R | 12 +++++- man/map_c.Rd | 68 +++++++++++++++++++++++++++++++++ tests/testthat/_snaps/map-df.md | 21 ++++++++++ tests/testthat/test-map-df.R | 10 +++++ 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 R/map-combine.R create mode 100644 man/map_c.Rd create mode 100644 tests/testthat/_snaps/map-df.md diff --git a/NAMESPACE b/NAMESPACE index 38557f92..201c294e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -149,7 +149,9 @@ export(map2_int) export(map2_lgl) export(map2_raw) export(map_at) +export(map_c) export(map_call) +export(map_cbind) export(map_chr) export(map_dbl) export(map_depth) @@ -160,6 +162,7 @@ export(map_if) export(map_int) export(map_lgl) export(map_raw) +export(map_rbind) export(modify) export(modify2) export(modify_at) diff --git a/R/map-combine.R b/R/map-combine.R new file mode 100644 index 00000000..da723950 --- /dev/null +++ b/R/map-combine.R @@ -0,0 +1,32 @@ +#' Apply a function to each element of a vector and combine the results +#' +#' These functions are variants of [map()] that combine the results with +#' [list_c()], [list_rbind()], and [list_cbind()] respectively. Compared +#' to `map()`, `.f` can return an output of any length, which means that +#' there's no longer a one-to-one correspondence between each element of the +#' input and each element of the output +#' +#' @inheritParams map +#' @inheritParams list_c +#' @export +#' @examples +#' map(1:3, ~ rep(.x, .x)) +#' map_c(1:3, ~ rep(.x, .x)) +map_c <- function(.x, .f, ..., .ptype = NULL) { + out <- map(.x, .f, ...) + list_c(out, ptype = .ptype) +} + +#' @export +#' @rdname map_c +map_rbind <- function(.x, .f, ..., .id = NULL, .ptype = NULL) { + out <- map(.x, .f, ...) + list_rbind(out, id = .id, ptype = .ptype) +} + +#' @export +#' @rdname map_c +map_cbind <- function(.x, .f, ..., .name_repair = c("unique", "universal", "check_unique"), .size = NULL) { + out <- map(.x, .f, ...) + list_cbind(out, name_repair = .name_repair, size = .size) +} diff --git a/R/map-df.R b/R/map-df.R index 594d9c18..17748e06 100644 --- a/R/map-df.R +++ b/R/map-df.R @@ -55,6 +55,7 @@ #' # now #' map2_cbind(arg1, arg2, ex_fun) map_dfr <- function(.x, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map_dfc()", "map_rbind()") check_installed("dplyr", "for `map_dfr()`.") .f <- as_mapper(.f, ...) @@ -65,11 +66,20 @@ map_dfr <- function(.x, .f, ..., .id = NULL) { #' @rdname map_dfr #' @usage NULL #' @export -map_df <- map_dfr +map_df <- function(.x, .f, ..., .id = NULL) { + lifecycle::deprecate_warn("0.4.0", "map_df()", "map_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()", "map_cbind()") + check_installed("dplyr", "for `map_dfc()`.") .f <- as_mapper(.f, ...) diff --git a/man/map_c.Rd b/man/map_c.Rd new file mode 100644 index 00000000..88295aae --- /dev/null +++ b/man/map_c.Rd @@ -0,0 +1,68 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/map-combine.R +\name{map_c} +\alias{map_c} +\alias{map_rbind} +\alias{map_cbind} +\title{Apply a function to each element of a vector and combine the results} +\usage{ +map_c(.x, .f, ..., .ptype = NULL) + +map_rbind(.x, .f, ..., .id = NULL, .ptype = NULL) + +map_cbind( + .x, + .f, + ..., + .name_repair = c("unique", "universal", "check_unique"), + .size = NULL +) +} +\arguments{ +\item{.x}{A list or atomic vector.} + +\item{.f}{A function, formula, or vector (not necessarily atomic). + +If a \strong{function}, it is used as is. + +If a \strong{formula}, e.g. \code{~ .x + 2}, it is converted to a function. There +are three ways to refer to the arguments: +\itemize{ +\item For a single argument function, use \code{.} +\item For a two argument function, use \code{.x} and \code{.y} +\item For more arguments, use \code{..1}, \code{..2}, \code{..3} etc +} + +This syntax allows you to create very compact anonymous +functions. Note that formula functions conceptually take dots +(that's why you can use \code{..1} etc). They silently ignore +additional arguments that are not used in the formula expression. + +If \strong{character vector}, \strong{numeric vector}, or \strong{list}, it is +converted to an extractor function. Character vectors index by +name and numeric vectors index by position; use a list to index +by position and name at different levels. If a component is not +present, the value of \code{.default} will be returned.} + +\item{...}{Additional arguments passed on to the mapped function.} + +\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).} +} +\description{ +These functions are variants of \code{\link[=map]{map()}} that combine the results with +\code{\link[=list_c]{list_c()}}, \code{\link[=list_rbind]{list_rbind()}}, and \code{\link[=list_cbind]{list_cbind()}} respectively. Compared +to \code{map()}, \code{.f} can return an output of any length, which means that +there's no longer a one-to-one correspondence between each element of the +input and each element of the output +} +\examples{ +map(1:3, ~ rep(.x, .x)) +map_c(1:3, ~ rep(.x, .x)) +} diff --git a/tests/testthat/_snaps/map-df.md b/tests/testthat/_snaps/map-df.md new file mode 100644 index 00000000..b7788f6d --- /dev/null +++ b/tests/testthat/_snaps/map-df.md @@ -0,0 +1,21 @@ +# _df/_dfc/_dfr are deprecated + + Code + . <- map_df(list(), identity) + Condition + Warning: + `map_df()` was deprecated in purrr 0.4.0. + Please use `map_rbind()` instead. + Code + . <- map_dfr(list(), identity) + Condition + Warning: + `map_dfc()` was deprecated in purrr 0.4.0. + Please use `map_rbind()` instead. + Code + . <- map_dfc(list(), identity) + Condition + Warning: + `map_dfc()` was deprecated in purrr 0.4.0. + Please use `map_cbind()` instead. + diff --git a/tests/testthat/test-map-df.R b/tests/testthat/test-map-df.R index 0668afb2..36620ee3 100644 --- a/tests/testthat/test-map-df.R +++ b/tests/testthat/test-map-df.R @@ -1,4 +1,14 @@ +test_that("_df/_dfc/_dfr are deprecated", { + expect_snapshot({ + . <- map_df(list(), identity) + . <- map_dfr(list(), identity) + . <- map_dfc(list(), identity) + }) +}) + test_that("row and column binding work", { + local_options(lifecycle_verbosity = "quiet") + skip_if_not_installed("dplyr") local_name_repair_quiet() From 7a618c0bf8650ad37ebc4a8c31ceb370e1346ed7 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sun, 4 Sep 2022 09:30:43 -0500 Subject: [PATCH 11/27] Get R CMD check passing again --- R/flatten.R | 1 + R/list-combine.R | 13 ++++++++++--- R/map-df.R | 4 ++-- _pkgdown.yml | 2 +- man/flatten.Rd | 1 + man/list_c.Rd | 14 +++++++++++--- man/map_c.Rd | 5 +++++ man/map_dfr.Rd | 4 ++-- 8 files changed, 33 insertions(+), 11 deletions(-) diff --git a/R/flatten.R b/R/flatten.R index 1d55cbc2..3a830d47 100644 --- a/R/flatten.R +++ b/R/flatten.R @@ -21,6 +21,7 @@ #' `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 diff --git a/R/list-combine.R b/R/list-combine.R index 0b167953..512fc71f 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -13,6 +13,10 @@ #' @param x A list. #' @param ptype An optional prototype to ensure that the output type is always #' the same. +#' @param id By default, `names(x)` are list. Alternatively, supply a string +#' an 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 thee 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"`. @@ -23,10 +27,13 @@ #' list_c(x1) #' #' x2 <- list( -#' data.frame(x = 1:2), -#' data.frame(y = "a") +#' 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) { check_is_list(x) @@ -46,7 +53,7 @@ list_cbind <- function( #' @export #' @rdname list_c -list_rbind <- function(x, id = NULL, ptype = NULL) { +list_rbind <- function(x, id = rlang::zap(), ptype = NULL) { check_is_list(x) vctrs::vec_rbind(!!!x, .names_to = id, .ptype = ptype) } diff --git a/R/map-df.R b/R/map-df.R index 17748e06..c4db17b3 100644 --- a/R/map-df.R +++ b/R/map-df.R @@ -48,12 +48,12 @@ #' # was #' map2_dfr(arg1, arg2, ex_fun) #' # now -#' map2_rbind(arg1, arg2, ex_fun) +#' # map2_rbind(arg1, arg2, ex_fun) #' #' # was #' map2_dfc(arg1, arg2, ex_fun) #' # now -#' map2_cbind(arg1, arg2, ex_fun) +#' # map2_cbind(arg1, arg2, ex_fun) map_dfr <- function(.x, .f, ..., .id = NULL) { lifecycle::deprecate_warn("0.4.0", "map_dfc()", "map_rbind()") check_installed("dplyr", "for `map_dfr()`.") diff --git a/_pkgdown.yml b/_pkgdown.yml index 2fdbb7d1..8284b6fd 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -36,6 +36,7 @@ reference: See details in `as_mapper()` contents: - map + - map_c - as_mapper - title: Map variants @@ -81,7 +82,6 @@ reference: A grab bag of useful tools for manipulating vectors. contents: - accumulate - - flatten - list_c - list_flatten - list_modify diff --git a/man/flatten.Rd b/man/flatten.Rd index 92488a2e..2032cb5b 100644 --- a/man/flatten.Rd +++ b/man/flatten.Rd @@ -66,3 +66,4 @@ x \%>\% flatten() \%>\% str() # now x \%>\% list_flatten() \%>\% str() } +\keyword{internal} diff --git a/man/list_c.Rd b/man/list_c.Rd index bdf3b266..aa8067e0 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -14,7 +14,7 @@ list_cbind( size = NULL ) -list_rbind(x, id = NULL, ptype = NULL) +list_rbind(x, id = rlang::zap(), ptype = NULL) } \arguments{ \item{x}{A list.} @@ -27,6 +27,11 @@ See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of t \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 list. Alternatively, supply a string +an 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 thee names.} } \description{ \itemize{ @@ -43,9 +48,12 @@ x1 <- list(a = 1, b = 2, c = 3) list_c(x1) x2 <- list( - data.frame(x = 1:2), - data.frame(y = "a") + 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/map_c.Rd b/man/map_c.Rd index 88295aae..a64a1056 100644 --- a/man/map_c.Rd +++ b/man/map_c.Rd @@ -49,6 +49,11 @@ present, the value of \code{.default} will be returned.} \item{.ptype}{An optional prototype to ensure that the output type is always the same.} +\item{.id}{By default, \code{names(x)} are list. Alternatively, supply a string +an 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 thee names.} + \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.} diff --git a/man/map_dfr.Rd b/man/map_dfr.Rd index a66f29ef..81eea3c5 100644 --- a/man/map_dfr.Rd +++ b/man/map_dfr.Rd @@ -78,11 +78,11 @@ arg2 <- 10:13 # was map2_dfr(arg1, arg2, ex_fun) # now -map2_rbind(arg1, arg2, ex_fun) +# map2_rbind(arg1, arg2, ex_fun) # was map2_dfc(arg1, arg2, ex_fun) # now -map2_cbind(arg1, arg2, ex_fun) +# map2_cbind(arg1, arg2, ex_fun) } \keyword{internal} From af378ad1c5fc33d9750640520c2fb24626639be9 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Sun, 4 Sep 2022 09:38:37 -0500 Subject: [PATCH 12/27] Fix narrowing of `.f` --- R/map.R | 8 ++------ man/map.Rd | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/R/map.R b/R/map.R index 6ea108de..8f8e6151 100644 --- a/R/map.R +++ b/R/map.R @@ -9,16 +9,12 @@ #' 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). +#' atomic vector of the indicated type (or die trying). For these functions, +#' `.f` must return a length-1 vector of the appropriate type. #' #' * `walk()` calls `.f` for its side-effect and returns #' the input `.x`. #' -#' 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`. -#' #' @inheritParams as_mapper #' @param .x A list or atomic vector. #' @param ... Additional arguments passed on to the mapped function. diff --git a/man/map.Rd b/man/map.Rd index e6f04a70..7fdf2d80 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -70,15 +70,11 @@ the same length as the input. \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). +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}. } - -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}. } \examples{ # Compute normal distributions from an atomic vector From 94d7324f9aa5fff6762d0440a54bb217527fcfc2 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 07:56:34 -0500 Subject: [PATCH 13/27] Apply suggestions from code review Co-authored-by: Davis Vaughan --- R/list-combine.R | 4 ++-- R/list-flatten.R | 2 +- R/map-df.R | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/list-combine.R b/R/list-combine.R index 512fc71f..8dac6d3d 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -14,9 +14,9 @@ #' @param ptype An optional prototype to ensure that the output type is always #' the same. #' @param id By default, `names(x)` are list. Alternatively, supply a string -#' an the names will be saved into a column with name `id`. If `id` +#' 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 thee names. +#' 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"`. diff --git a/R/list-flatten.R b/R/list-flatten.R index 6dc93b6b..68806b79 100644 --- a/R/list-flatten.R +++ b/R/list-flatten.R @@ -37,6 +37,6 @@ list_flatten <- function(x, name_spec = "{outer}_{inner}") { check_is_list(x) - x <- modify_if(x, vctrs::vec_is_list, identity, .else = list) + x <- modify_if(x, vec_is_list, identity, .else = list) vec_unchop(x, ptype = list(), name_spec = name_spec) } diff --git a/R/map-df.R b/R/map-df.R index c4db17b3..d538c87e 100644 --- a/R/map-df.R +++ b/R/map-df.R @@ -5,10 +5,10 @@ #' #' 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 have often confusing semantics, and their +#' 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 accept results -#' of anything because the results are combined to together. +#' 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. #' #' You can now instead use functions with `_rbind()` and `_cbind()` suffixes #' which use `vctrs::vec_rbind()` and `vctrs::vec_cbind()` under the hood, From d687da00f685795faf858a89a0b5fbcb1c45552f Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 07:55:58 -0500 Subject: [PATCH 14/27] Remove map wrappers --- NAMESPACE | 3 -- R/map-combine.R | 32 ---------------------- man/map_c.Rd | 73 ------------------------------------------------- 3 files changed, 108 deletions(-) delete mode 100644 R/map-combine.R delete mode 100644 man/map_c.Rd diff --git a/NAMESPACE b/NAMESPACE index 201c294e..38557f92 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -149,9 +149,7 @@ export(map2_int) export(map2_lgl) export(map2_raw) export(map_at) -export(map_c) export(map_call) -export(map_cbind) export(map_chr) export(map_dbl) export(map_depth) @@ -162,7 +160,6 @@ export(map_if) export(map_int) export(map_lgl) export(map_raw) -export(map_rbind) export(modify) export(modify2) export(modify_at) diff --git a/R/map-combine.R b/R/map-combine.R deleted file mode 100644 index da723950..00000000 --- a/R/map-combine.R +++ /dev/null @@ -1,32 +0,0 @@ -#' Apply a function to each element of a vector and combine the results -#' -#' These functions are variants of [map()] that combine the results with -#' [list_c()], [list_rbind()], and [list_cbind()] respectively. Compared -#' to `map()`, `.f` can return an output of any length, which means that -#' there's no longer a one-to-one correspondence between each element of the -#' input and each element of the output -#' -#' @inheritParams map -#' @inheritParams list_c -#' @export -#' @examples -#' map(1:3, ~ rep(.x, .x)) -#' map_c(1:3, ~ rep(.x, .x)) -map_c <- function(.x, .f, ..., .ptype = NULL) { - out <- map(.x, .f, ...) - list_c(out, ptype = .ptype) -} - -#' @export -#' @rdname map_c -map_rbind <- function(.x, .f, ..., .id = NULL, .ptype = NULL) { - out <- map(.x, .f, ...) - list_rbind(out, id = .id, ptype = .ptype) -} - -#' @export -#' @rdname map_c -map_cbind <- function(.x, .f, ..., .name_repair = c("unique", "universal", "check_unique"), .size = NULL) { - out <- map(.x, .f, ...) - list_cbind(out, name_repair = .name_repair, size = .size) -} diff --git a/man/map_c.Rd b/man/map_c.Rd deleted file mode 100644 index a64a1056..00000000 --- a/man/map_c.Rd +++ /dev/null @@ -1,73 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/map-combine.R -\name{map_c} -\alias{map_c} -\alias{map_rbind} -\alias{map_cbind} -\title{Apply a function to each element of a vector and combine the results} -\usage{ -map_c(.x, .f, ..., .ptype = NULL) - -map_rbind(.x, .f, ..., .id = NULL, .ptype = NULL) - -map_cbind( - .x, - .f, - ..., - .name_repair = c("unique", "universal", "check_unique"), - .size = NULL -) -} -\arguments{ -\item{.x}{A list or atomic vector.} - -\item{.f}{A function, formula, or vector (not necessarily atomic). - -If a \strong{function}, it is used as is. - -If a \strong{formula}, e.g. \code{~ .x + 2}, it is converted to a function. There -are three ways to refer to the arguments: -\itemize{ -\item For a single argument function, use \code{.} -\item For a two argument function, use \code{.x} and \code{.y} -\item For more arguments, use \code{..1}, \code{..2}, \code{..3} etc -} - -This syntax allows you to create very compact anonymous -functions. Note that formula functions conceptually take dots -(that's why you can use \code{..1} etc). They silently ignore -additional arguments that are not used in the formula expression. - -If \strong{character vector}, \strong{numeric vector}, or \strong{list}, it is -converted to an extractor function. Character vectors index by -name and numeric vectors index by position; use a list to index -by position and name at different levels. If a component is not -present, the value of \code{.default} will be returned.} - -\item{...}{Additional arguments passed on to the mapped function.} - -\item{.ptype}{An optional prototype to ensure that the output type is always -the same.} - -\item{.id}{By default, \code{names(x)} are list. Alternatively, supply a string -an 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 thee names.} - -\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).} -} -\description{ -These functions are variants of \code{\link[=map]{map()}} that combine the results with -\code{\link[=list_c]{list_c()}}, \code{\link[=list_rbind]{list_rbind()}}, and \code{\link[=list_cbind]{list_cbind()}} respectively. Compared -to \code{map()}, \code{.f} can return an output of any length, which means that -there's no longer a one-to-one correspondence between each element of the -input and each element of the output -} -\examples{ -map(1:3, ~ rep(.x, .x)) -map_c(1:3, ~ rep(.x, .x)) -} From 1e143d22d8190d005514713f1547223e60a80520 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 07:56:53 -0500 Subject: [PATCH 15/27] Re-document --- man/list_c.Rd | 4 ++-- man/map_dfr.Rd | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/man/list_c.Rd b/man/list_c.Rd index aa8067e0..1e9f708c 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -29,9 +29,9 @@ See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of t same size (i.e. number of rows).} \item{id}{By default, \code{names(x)} are list. Alternatively, supply a string -an the names will be saved into a column with name \code{id}. If \code{id} +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 thee names.} +be used instead of the names.} } \description{ \itemize{ diff --git a/man/map_dfr.Rd b/man/map_dfr.Rd index 81eea3c5..2feacb1a 100644 --- a/man/map_dfr.Rd +++ b/man/map_dfr.Rd @@ -43,10 +43,10 @@ Only applies to \verb{_dfr} variant.} 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 have often confusing semantics, and their +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 accept results -of anything because the results are combined to together. +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. You can now instead use functions with \verb{_rbind()} and \verb{_cbind()} suffixes which use \code{vctrs::vec_rbind()} and \code{vctrs::vec_cbind()} under the hood, From 0fee03ec99ee0e6e8f7c851f738b23877615ac95 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:00:54 -0500 Subject: [PATCH 16/27] Improve error reporting --- DESCRIPTION | 7 ++++--- R/list-combine.R | 19 +++++-------------- tests/testthat/_snaps/list-combine.md | 12 ++++++------ 3 files changed, 15 insertions(+), 23 deletions(-) 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/R/list-combine.R b/R/list-combine.R index 8dac6d3d..605651fa 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -36,7 +36,7 @@ #' #' list_cbind(x2) list_c <- function(x, ptype = NULL) { - check_is_list(x) + vec_check_list(x) vctrs::vec_unchop(x, ptype = ptype) } @@ -47,22 +47,13 @@ list_cbind <- function( name_repair = c("unique", "universal", "check_unique"), size = NULL ) { - check_is_list(x) - vctrs::vec_cbind(!!!x, .name_repair = name_repair, .size = size) + vec_check_list(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_is_list(x) - vctrs::vec_rbind(!!!x, .names_to = id, .ptype = ptype) -} - -check_is_list <- function(x, error_call = caller_env()) { - if (!vctrs::vec_is_list(x)) { - cli::cli_abort( - "{.arg x} must be a list, not {friendly_type_of(x)}", - call = error_call - ) - } + vec_check_list(x) + vctrs::vec_rbind(!!!x, .names_to = id, .ptype = ptype, .call = current_env()) } diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md index e91399db..dcb6a49c 100644 --- a/tests/testthat/_snaps/list-combine.md +++ b/tests/testthat/_snaps/list-combine.md @@ -19,7 +19,7 @@ Code list_cbind(list(df1, df3)) Condition - Error in `vctrs::vec_cbind()`: + Error in `list_cbind()`: ! Can't recycle `..1` (size 2) to match `..2` (size 3). # list_cbind() can enforce size @@ -35,7 +35,7 @@ Code list_rbind(list(df1, df3)) Condition - Error in `vctrs::vec_rbind()`: + Error in `list_rbind()`: ! Can't combine `..1$x` and `..2$x` . # list_rbind() can enforce ptype @@ -43,7 +43,7 @@ Code list_rbind(list(df1), ptype = data.frame(x = character())) Condition - Error in `vctrs::vec_rbind()`: + Error in `list_rbind()`: ! Can't convert `..1$x` to match type of `x` . # assert input is a list @@ -52,15 +52,15 @@ list_c(1) Condition Error in `list_c()`: - ! `x` must be a list, not a double vector + ! `x` must be a list, not a number. Code list_rbind(1) Condition Error in `list_rbind()`: - ! `x` must be a list, not a double vector + ! `x` must be a list, not a number. Code list_cbind(1) Condition Error in `list_cbind()`: - ! `x` must be a list, not a double vector + ! `x` must be a list, not a number. From b6b939eddf3b45f5bbcc74821341934a349429b1 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:03:19 -0500 Subject: [PATCH 17/27] list_flatten() improvements --- R/list-flatten.R | 21 +++++++++++++++++---- man/list_flatten.Rd | 13 +++++++++++-- tests/testthat/_snaps/list-flatten.md | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/R/list-flatten.R b/R/list-flatten.R index 68806b79..1ae13ad9 100644 --- a/R/list-flatten.R +++ b/R/list-flatten.R @@ -1,11 +1,15 @@ #' Flatten a list #' -#' Flattening a list removes a single layer of internal hierarchy. +#' 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. @@ -34,9 +38,18 @@ #' 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}") { - check_is_list(x) +list_flatten <- function( + x, + name_spec = "{outer}_{inner}", + name_repair = c("minimal", "unique", "check_unique", "universal") + ) { + vec_check_list(x) x <- modify_if(x, vec_is_list, identity, .else = list) - vec_unchop(x, ptype = list(), name_spec = name_spec) + vec_unchop( + x, + ptype = list(), + name_spec = name_spec, + name_repair = name_repair + ) } diff --git a/man/list_flatten.Rd b/man/list_flatten.Rd index 5fb5197d..0a7b7cca 100644 --- a/man/list_flatten.Rd +++ b/man/list_flatten.Rd @@ -4,7 +4,11 @@ \alias{list_flatten} \title{Flatten a list} \usage{ -list_flatten(x, name_spec = "{outer}_{inner}") +list_flatten( + x, + name_spec = "{outer}_{inner}", + name_repair = c("minimal", "unique", "check_unique", "universal") +) } \arguments{ \item{x}{A list.} @@ -12,6 +16,10 @@ list_flatten(x, name_spec = "{outer}_{inner}") \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, @@ -19,7 +27,8 @@ 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. +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))) diff --git a/tests/testthat/_snaps/list-flatten.md b/tests/testthat/_snaps/list-flatten.md index 001c26cc..4cc08ba0 100644 --- a/tests/testthat/_snaps/list-flatten.md +++ b/tests/testthat/_snaps/list-flatten.md @@ -4,5 +4,5 @@ list_flatten(1:2) Condition Error in `list_flatten()`: - ! `x` must be a list, not an integer vector + ! `x` must be a list, not an integer vector. From 32ce653b891890c701665f0cb3e77cf37c216f3b Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:08:27 -0500 Subject: [PATCH 18/27] Doc updates --- R/list-combine.R | 2 +- R/map-df.R | 13 +++++++------ man/list_c.Rd | 2 +- man/map_dfr.Rd | 13 +++++++------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/R/list-combine.R b/R/list-combine.R index 605651fa..401693b4 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -13,7 +13,7 @@ #' @param x A list. #' @param ptype An optional prototype to ensure that the output type is always #' the same. -#' @param id By default, `names(x)` are list. Alternatively, supply a string +#' @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. diff --git a/R/map-df.R b/R/map-df.R index d538c87e..86ee1c69 100644 --- a/R/map-df.R +++ b/R/map-df.R @@ -10,9 +10,9 @@ #' 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. #' -#' You can now instead use functions with `_rbind()` and `_cbind()` suffixes -#' which use `vctrs::vec_rbind()` and `vctrs::vec_cbind()` under the hood, -#' and have names that more clearly reflect their semantics. +#' 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 @@ -34,7 +34,8 @@ #' mtcars %>% #' split(.$cyl) %>% #' map(~ lm(mpg ~ wt, data = .x)) %>% -#' map_rbind(~ as.data.frame(t(as.matrix(coef(.))))) +#' map(~ as.data.frame(t(as.matrix(coef(.))))) %>% +#' list_rbind() #' #' # map2 --------------------------------------------- #' @@ -48,12 +49,12 @@ #' # was #' map2_dfr(arg1, arg2, ex_fun) #' # now -#' # map2_rbind(arg1, arg2, ex_fun) +#' map2(arg1, arg2, ex_fun) %>% list_rbind() #' #' # was #' map2_dfc(arg1, arg2, ex_fun) #' # now -#' # map2_cbind(arg1, arg2, ex_fun) +#' map2(arg1, arg2, ex_fun) %>% list_cbind() map_dfr <- function(.x, .f, ..., .id = NULL) { lifecycle::deprecate_warn("0.4.0", "map_dfc()", "map_rbind()") check_installed("dplyr", "for `map_dfr()`.") diff --git a/man/list_c.Rd b/man/list_c.Rd index 1e9f708c..b5155512 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -28,7 +28,7 @@ See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of t \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 list. Alternatively, supply a string +\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.} diff --git a/man/map_dfr.Rd b/man/map_dfr.Rd index 2feacb1a..19d764b5 100644 --- a/man/map_dfr.Rd +++ b/man/map_dfr.Rd @@ -48,9 +48,9 @@ names are suboptimal because they suggest they work like \verb{_lgl()}, \verb{_i 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. -You can now instead use functions with \verb{_rbind()} and \verb{_cbind()} suffixes -which use \code{vctrs::vec_rbind()} and \code{vctrs::vec_cbind()} under the hood, -and have names that more clearly reflect their semantics. +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 --------------------------------------------- @@ -64,7 +64,8 @@ mtcars \%>\% mtcars \%>\% split(.$cyl) \%>\% map(~ lm(mpg ~ wt, data = .x)) \%>\% - map_rbind(~ as.data.frame(t(as.matrix(coef(.))))) + map(~ as.data.frame(t(as.matrix(coef(.))))) \%>\% + list_rbind() # map2 --------------------------------------------- @@ -78,11 +79,11 @@ arg2 <- 10:13 # was map2_dfr(arg1, arg2, ex_fun) # now -# map2_rbind(arg1, arg2, ex_fun) +map2(arg1, arg2, ex_fun) \%>\% list_rbind() # was map2_dfc(arg1, arg2, ex_fun) # now -# map2_cbind(arg1, arg2, ex_fun) +map2(arg1, arg2, ex_fun) \%>\% list_cbind() } \keyword{internal} From cf301aedc8e1c115bd0e1cb88eb0bf8da037ff54 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:08:35 -0500 Subject: [PATCH 19/27] Test that data frames aren't ok --- tests/testthat/_snaps/list-combine.md | 18 ++++++++++++++++++ tests/testthat/test-list-combine.R | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md index dcb6a49c..c3e2c953 100644 --- a/tests/testthat/_snaps/list-combine.md +++ b/tests/testthat/_snaps/list-combine.md @@ -64,3 +64,21 @@ 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. + diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R index d3aa81f4..3b4505f9 100644 --- a/tests/testthat/test-list-combine.R +++ b/tests/testthat/test-list-combine.R @@ -56,4 +56,11 @@ test_that("assert input is a list", { 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) + }) }) From e058e7117c99e8f7cd683a392b9190e7a689d0bb Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:13:03 -0500 Subject: [PATCH 20/27] stringsAsFactors = ugh --- tests/testthat/_snaps/list-combine.md | 3 ++- tests/testthat/test-list-combine.R | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md index c3e2c953..dc0357be 100644 --- a/tests/testthat/_snaps/list-combine.md +++ b/tests/testthat/_snaps/list-combine.md @@ -41,7 +41,8 @@ # list_rbind() can enforce ptype Code - list_rbind(list(df1), ptype = data.frame(x = character())) + 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` . diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R index 3b4505f9..3b869b3f 100644 --- a/tests/testthat/test-list-combine.R +++ b/tests/testthat/test-list-combine.R @@ -34,7 +34,7 @@ test_that("list_cbind() can enforce size", { 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") + df3 <- data.frame(x = "a", stringsAsFactors = FALSE) expect_equal(list_rbind(list(df1, df2)), data.frame(x = 1:2, y = c(NA, 1))) @@ -45,8 +45,10 @@ test_that("list_rbind() row-binds compatible data.frames", { test_that("list_rbind() can enforce ptype", { df1 <- data.frame(x = 1) + expect_snapshot(error = TRUE, { - list_rbind(list(df1), ptype = data.frame(x = character())) + ptype <- data.frame(x = character(), stringsAsFactors = FALSE) + list_rbind(list(df1), ptype = ptype) }) }) From 68c72eb8281ac8755e2736497ea2bd93bc1f89df Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:15:04 -0500 Subject: [PATCH 21/27] Update news and function reference --- NEWS.md | 3 +++ _pkgdown.yml | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 1d9d5ffc..86d97bc8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -40,6 +40,9 @@ * `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 diff --git a/_pkgdown.yml b/_pkgdown.yml index d000d6c4..2fbf5c52 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -36,7 +36,6 @@ reference: See details in `as_mapper()` contents: - map - - map_c - as_mapper - title: Map variants From a209d5cd806b26ec13b707a860b15505e8ccaec5 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:21:52 -0500 Subject: [PATCH 22/27] Tweak docs --- R/map.R | 6 +++--- man/map.Rd | 6 +++--- man/map2.Rd | 6 +++--- man/pmap.Rd | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/R/map.R b/R/map.R index ab4355d9..f5d51103 100644 --- a/R/map.R +++ b/R/map.R @@ -33,15 +33,15 @@ #' 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. The output of `.f` will only be #' automatically coerced upwards (i.e. logical -> integer -> double -> -#' character). It will be named if the input was named. +#' character). #' #' * `walk()` returns the input `.x` (invisibly). This makes it easy to #' use in a pipe. diff --git a/man/map.Rd b/man/map.Rd index 6f3397bf..4831fca5 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -43,14 +43,14 @@ Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} } \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. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> -character). It will be named if the input was named. +character). \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to use in a pipe. } diff --git a/man/map2.Rd b/man/map2.Rd index f6447d84..2dab8e97 100644 --- a/man/map2.Rd +++ b/man/map2.Rd @@ -41,14 +41,14 @@ Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} } \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. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> -character). It will be named if the input was named. +character). \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to use in a pipe. } diff --git a/man/pmap.Rd b/man/pmap.Rd index d2333f68..248743c2 100644 --- a/man/pmap.Rd +++ b/man/pmap.Rd @@ -47,14 +47,14 @@ Note that the arguments that differ in each call come before \code{.f}, and the arguments that are the same come after \code{.f}.} } \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. The output of \code{.f} will only be automatically coerced upwards (i.e. logical -> integer -> double -> -character). It will be named if the input was named. +character). \item \code{walk()} returns the input \code{.x} (invisibly). This makes it easy to use in a pipe. } From 10077a4ec60fcfc35c6f683b12013f9e1565d4d9 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 08:59:47 -0500 Subject: [PATCH 23/27] Doc tweak --- R/list-combine.R | 2 +- man/list_c.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/list-combine.R b/R/list-combine.R index 401693b4..ec3bb7b3 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -14,7 +14,7 @@ #' @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` +#' 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 diff --git a/man/list_c.Rd b/man/list_c.Rd index b5155512..a747f55c 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -29,7 +29,7 @@ See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of t 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} +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.} } From 91a76f0ee0c97052e6f3e7584bcadcb6bdcf2095 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 09:06:41 -0500 Subject: [PATCH 24/27] Update/finish deprecation messages --- R/map-df.R | 39 ++++++++++++++++++----- tests/testthat/_snaps/map-df.md | 56 ++++++++++++++++++++++++++++++--- tests/testthat/test-map-df.R | 13 ++++++++ 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/R/map-df.R b/R/map-df.R index 86ee1c69..dbefd6b3 100644 --- a/R/map-df.R +++ b/R/map-df.R @@ -56,7 +56,7 @@ #' # now #' map2(arg1, arg2, ex_fun) %>% list_cbind() map_dfr <- function(.x, .f, ..., .id = NULL) { - lifecycle::deprecate_warn("0.4.0", "map_dfc()", "map_rbind()") + lifecycle::deprecate_warn("0.4.0", "map_dfr()", I("`map()` + `list_rbind()`")) check_installed("dplyr", "for `map_dfr()`.") .f <- as_mapper(.f, ...) @@ -68,7 +68,7 @@ map_dfr <- function(.x, .f, ..., .id = NULL) { #' @usage NULL #' @export map_df <- function(.x, .f, ..., .id = NULL) { - lifecycle::deprecate_warn("0.4.0", "map_df()", "map_rbind()") + lifecycle::deprecate_warn("0.4.0", "map_df()", I("`map()` + `list_rbind()`")) check_installed("dplyr", "for `map_dfr()`.") .f <- as_mapper(.f, ...) @@ -79,8 +79,7 @@ map_df <- function(.x, .f, ..., .id = NULL) { #' @rdname map_dfr #' @export map_dfc <- function(.x, .f, ...) { - lifecycle::deprecate_warn("0.4.0", "map_dfc()", "map_cbind()") - + lifecycle::deprecate_warn("0.4.0", "map_dfc()", I("`map()` + `list_cbind()`")) check_installed("dplyr", "for `map_dfc()`.") .f <- as_mapper(.f, ...) @@ -91,20 +90,27 @@ map_dfc <- function(.x, .f, ...) { #' @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, ...) - map2_dfr(.x, vec_index(.x), .f, ..., .id = .id) + 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, ...) - map2_dfc(.x, vec_index(.x), .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, ...) @@ -115,6 +121,7 @@ map2_dfr <- function(.x, .y, .f, ..., .id = NULL) { #' @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, ...) @@ -125,11 +132,19 @@ map2_dfc <- function(.x, .y, .f, ...) { #' @rdname map_dfr #' @export #' @usage NULL -map2_df <- map2_dfr +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, ...) @@ -140,6 +155,7 @@ pmap_dfr <- function(.l, .f, ..., .id = NULL) { #' @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, ...) @@ -150,4 +166,11 @@ pmap_dfc <- function(.l, .f, ...) { #' @rdname map_dfr #' @export #' @usage NULL -pmap_df <- pmap_dfr +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/tests/testthat/_snaps/map-df.md b/tests/testthat/_snaps/map-df.md index b7788f6d..85070c4a 100644 --- a/tests/testthat/_snaps/map-df.md +++ b/tests/testthat/_snaps/map-df.md @@ -5,17 +5,65 @@ Condition Warning: `map_df()` was deprecated in purrr 0.4.0. - Please use `map_rbind()` instead. + Please use `map()` + `list_rbind()` instead. Code . <- map_dfr(list(), identity) Condition Warning: - `map_dfc()` was deprecated in purrr 0.4.0. - Please use `map_rbind()` instead. + `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_cbind()` instead. + 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/test-map-df.R b/tests/testthat/test-map-df.R index 36620ee3..ae0cfec8 100644 --- a/tests/testthat/test-map-df.R +++ b/tests/testthat/test-map-df.R @@ -3,6 +3,17 @@ test_that("_df/_dfc/_dfr are deprecated", { . <- 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) }) }) @@ -22,12 +33,14 @@ test_that("row and column binding work", { }) 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() From eb288c97798631afeab5345f6f96756e688506db Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 09:07:00 -0500 Subject: [PATCH 25/27] Use map_if instead of modify_if --- R/list-flatten.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/list-flatten.R b/R/list-flatten.R index 1ae13ad9..0699b48f 100644 --- a/R/list-flatten.R +++ b/R/list-flatten.R @@ -45,7 +45,7 @@ list_flatten <- function( ) { vec_check_list(x) - x <- modify_if(x, vec_is_list, identity, .else = list) + x <- map_if(x, vec_is_list, identity, .else = list) vec_unchop( x, ptype = list(), From c71fcc8f7eecd0862668bad1c8ac9fcd5799aee1 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 09:12:22 -0500 Subject: [PATCH 26/27] Require data frames --- R/list-combine.R | 26 +++++++++++++++++++++++--- man/list_c.Rd | 3 ++- tests/testthat/_snaps/list-combine.md | 13 +++++++++++++ tests/testthat/test-list-combine.R | 7 +++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/R/list-combine.R b/R/list-combine.R index ec3bb7b3..c758d11d 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -10,7 +10,8 @@ #' * `list_cbind()` combines elements into a data frame by column-binding them #' together with [vctrs::vec_cbind()]. #' -#' @param x A list. +#' @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 @@ -47,13 +48,32 @@ list_cbind <- function( name_repair = c("unique", "universal", "check_unique"), size = NULL ) { - vec_check_list(x) + 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) { - vec_check_list(x) + 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/man/list_c.Rd b/man/list_c.Rd index a747f55c..52baac02 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -17,7 +17,8 @@ list_cbind( list_rbind(x, id = rlang::zap(), ptype = NULL) } \arguments{ -\item{x}{A list.} +\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.} diff --git a/tests/testthat/_snaps/list-combine.md b/tests/testthat/_snaps/list-combine.md index dc0357be..97ea38a8 100644 --- a/tests/testthat/_snaps/list-combine.md +++ b/tests/testthat/_snaps/list-combine.md @@ -83,3 +83,16 @@ 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/test-list-combine.R b/tests/testthat/test-list-combine.R index 3b869b3f..6b80c8a5 100644 --- a/tests/testthat/test-list-combine.R +++ b/tests/testthat/test-list-combine.R @@ -66,3 +66,10 @@ test_that("assert input is a list", { 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)) + }) +}) From dc8ca0232e3ca56204223ffc1d29d6938988ee13 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Thu, 8 Sep 2022 16:24:23 -0500 Subject: [PATCH 27/27] Doc fixes --- R/list-combine.R | 2 +- R/reduce.R | 5 +++-- man/accumulate.Rd | 5 +++-- man/list_c.Rd | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/R/list-combine.R b/R/list-combine.R index c758d11d..349f05a8 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -10,7 +10,7 @@ #' * `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 +#' @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. diff --git a/R/reduce.R b/R/reduce.R index f56b66b2..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_rbind(~ 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/accumulate.Rd b/man/accumulate.Rd index abedf12f..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_rbind(~ 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/list_c.Rd b/man/list_c.Rd index 52baac02..d9b6b784 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -17,7 +17,7 @@ list_cbind( list_rbind(x, id = rlang::zap(), ptype = NULL) } \arguments{ -\item{x}{A list. For \code{list_rbind()} and \code{list_cbind()} the list must +\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