Skip to content

Commit 50c58dd

Browse files
authored
Deprecate optional .cols and .fns cases (#6523)
* Remove defaults for `.cols` and `.fns` * Internally always supply `.cols` and `.fns` where applicable * Test that no arguments cases still work * NEWS bullet * Deprecate missing `.cols` everywhere With some complex changes to ensure that warnings thrown outside of any group context still bubble up correctly * Revise NEWS bullet
1 parent 02ef1d7 commit 50c58dd

File tree

8 files changed

+386
-45
lines changed

8 files changed

+386
-45
lines changed

NEWS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# dplyr (development version)
22

3+
* `.cols` and `.fns` are now required arguments in `across()`, `c_across()`,
4+
`if_any()`, and `if_all()`. In general, we now recommend that you use `pick()`
5+
instead of empty calls to `across()` (i.e. with no arguments) or
6+
`across(c(x, y))` (i.e. with no `.fns`) to select a subset of columns without
7+
applying any transformation (#6523).
8+
9+
* Relying on the previous default of `.cols = everything()` is deprecated.
10+
We have skipped the soft-deprecation stage in this case, because indirect
11+
usage of `across()` and friends in this way is rare.
12+
13+
* Relying on the previous default of `.fns = NULL` is not yet formally
14+
soft-deprecated, because there was no good alternative until now, but is
15+
discouraged and will be soft-deprecated in the next minor release.
16+
317
* `cur_data()` and `cur_data_all()` are now soft-deprecated in favor of
418
`pick()` (#6204).
519

R/across.R

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#' the predicate is `TRUE` for *any* of the selected columns, `if_all()`
1313
#' is `TRUE` when the predicate is `TRUE` for *all* selected columns.
1414
#'
15+
#' If you just need to select columns without applying a transformation to each
16+
#' of them, then you probably want to use [pick()] instead.
17+
#'
1518
#' `across()` supersedes the family of "scoped variants" like
1619
#' `summarise_at()`, `summarise_if()`, and `summarise_all()`.
1720
#'
@@ -27,9 +30,6 @@
2730
#' `list(mean = mean, n_miss = ~ sum(is.na(.x))`. Each function is applied
2831
#' to each column, and the output is named by combining the function name
2932
#' and the column name using the glue specification in `.names`.
30-
#' - `NULL`: the default, returns the selected columns in a data frame
31-
#' without applying a transformation. This is useful for when you want to
32-
#' use a function that takes a data frame.
3333
#'
3434
#' Within these functions you can use [cur_column()] and [cur_group()]
3535
#' to access the current column and grouping keys respectively.
@@ -168,14 +168,26 @@
168168
#'
169169
#' @export
170170
#' @seealso [c_across()] for a function that returns a vector
171-
across <- function(.cols = everything(),
172-
.fns = NULL,
171+
across <- function(.cols,
172+
.fns,
173173
...,
174174
.names = NULL,
175175
.unpack = FALSE) {
176176
mask <- peek_mask()
177177
caller_env <- caller_env()
178178

179+
.cols <- enquo(.cols)
180+
181+
if (quo_is_missing(.cols)) {
182+
across_missing_cols_deprecate_warn()
183+
.cols <- quo_set_expr(.cols, expr(everything()))
184+
}
185+
if (is_missing(.fns)) {
186+
# Silent restoration to old defaults of `.fns` for now.
187+
# TODO: Escalate this to formal deprecation.
188+
.fns <- NULL
189+
}
190+
179191
if (!is_bool(.unpack) && !is_string(.unpack)) {
180192
stop_input_type(.unpack, "`TRUE`, `FALSE`, or a single string")
181193
}
@@ -188,7 +200,7 @@ across <- function(.cols = everything(),
188200
}
189201

190202
setup <- across_setup(
191-
{{ .cols }},
203+
cols = !!.cols,
192204
fns = .fns,
193205
names = .names,
194206
.caller_env = caller_env,
@@ -221,6 +233,7 @@ across <- function(.cols = everything(),
221233
names <- setup$names
222234

223235
if (is.null(fns)) {
236+
# TODO: Deprecate and remove the `.fns = NULL` path in favor of `pick()`
224237
data <- mask$pick_current(vars)
225238

226239
if (is.null(names)) {
@@ -281,13 +294,13 @@ across <- function(.cols = everything(),
281294

282295
#' @rdname across
283296
#' @export
284-
if_any <- function(.cols = everything(), .fns = NULL, ..., .names = NULL) {
297+
if_any <- function(.cols, .fns, ..., .names = NULL) {
285298
context_local("across_if_fn", "if_any")
286299
if_across(`|`, across({{ .cols }}, .fns, ..., .names = .names))
287300
}
288301
#' @rdname across
289302
#' @export
290-
if_all <- function(.cols = everything(), .fns = NULL, ..., .names = NULL) {
303+
if_all <- function(.cols, .fns, ..., .names = NULL) {
291304
context_local("across_if_fn", "if_all")
292305
if_across(`&`, across({{ .cols }}, .fns, ..., .names = .names))
293306
}
@@ -330,13 +343,17 @@ if_across <- function(op, df) {
330343
#' mutate(
331344
#' sum = sum(c_across(w:z)),
332345
#' sd = sd(c_across(w:z))
333-
#' )
334-
c_across <- function(cols = everything()) {
346+
#' )
347+
c_across <- function(cols) {
348+
mask <- peek_mask()
335349
cols <- enquo(cols)
336350

337-
mask <- peek_mask()
338-
vars <- c_across_setup(!!cols, mask = mask)
351+
if (quo_is_missing(cols)) {
352+
c_across_missing_cols_deprecate_warn()
353+
cols <- quo_set_expr(cols, expr(everything()))
354+
}
339355

356+
vars <- c_across_setup(!!cols, mask = mask)
340357

341358
cols <- mask$current_cols(vars)
342359
vec_c(!!!cols, .name_spec = zap())
@@ -387,6 +404,7 @@ across_setup <- function(cols,
387404
vars <- names(data)[vars]
388405

389406
if (is.null(fns)) {
407+
# TODO: Eventually deprecate and remove the `.fns = NULL` path in favor of `pick()`
390408
if (!is.null(names)) {
391409
glue_mask <- across_glue_mask(.caller_env, .col = names_vars, .fn = "1")
392410
names <- vec_as_names(
@@ -411,7 +429,7 @@ across_setup <- function(cols,
411429
}
412430

413431
if (!is.list(fns)) {
414-
msg <- c("`.fns` must be NULL, a function, a formula, or a list of functions/formulas.")
432+
msg <- c("`.fns` must be a function, a formula, or a list of functions/formulas.")
415433
abort(msg, call = call(across_if_fn))
416434
}
417435

@@ -614,13 +632,22 @@ expand_across <- function(quo) {
614632
if (".cols" %in% names(expr)) {
615633
cols <- expr$.cols
616634
} else {
617-
cols <- quote(everything())
635+
across_missing_cols_deprecate_warn()
636+
cols <- expr(everything())
618637
}
619638
cols <- as_quosure(cols, env)
620639

640+
if (".fns" %in% names(expr)) {
641+
fns <- eval_tidy(expr$.fns, mask, env = env)
642+
} else {
643+
# In the missing case, silently restore the old default of `NULL`.
644+
# TODO: Escalate this to formal deprecation.
645+
fns <- NULL
646+
}
647+
621648
setup <- across_setup(
622649
!!cols,
623-
fns = eval_tidy(expr$.fns, mask, env = env),
650+
fns = fns,
624651
names = eval_tidy(expr$.names, mask, env = env),
625652
.caller_env = env,
626653
mask = dplyr_mask,
@@ -639,6 +666,7 @@ expand_across <- function(quo) {
639666

640667
# No functions, so just return a list of symbols
641668
if (is.null(fns)) {
669+
# TODO: Deprecate and remove the `.fns = NULL` path in favor of `pick()`
642670
expressions <- pmap(list(vars, names, seq_along(vars)), function(var, name, k) {
643671
quo <- new_quosure(sym(var), empty_env())
644672
quo <- new_dplyr_quosure(
@@ -738,6 +766,45 @@ is_inlinable_formula <- function(x, mask) {
738766
}
739767
}
740768

769+
across_missing_cols_deprecate_warn <- function() {
770+
across_if_fn <- context_peek_bare("across_if_fn") %||% "across"
771+
772+
# Passing the correct `user_env` through `expand_across()` to here is
773+
# complicated, so instead we force the global environment. This means users
774+
# won't ever see the "deprecated feature was likely used in the {pkg}"
775+
# message, but the warning will still fire and that is more important.
776+
user_env <- global_env()
777+
778+
cnd <- catch_cnd(classes = "lifecycle_warning_deprecated", {
779+
lifecycle::deprecate_warn(
780+
when = "1.1.0",
781+
what = I(glue("Using `{across_if_fn}()` without supplying `.cols`")),
782+
details = "Please supply `.cols` instead.",
783+
user_env = user_env
784+
)
785+
})
786+
787+
if (is_null(cnd)) {
788+
# Condition wasn't signaled
789+
return(NULL)
790+
}
791+
792+
# Subclassed so we can skip computing group context info when the warning
793+
# is thrown from `expand_across()` outside of any group
794+
class(cnd) <- c("dplyr:::warning_across_missing_cols_deprecated", class(cnd))
795+
796+
cnd_signal(cnd)
797+
}
798+
799+
c_across_missing_cols_deprecate_warn <- function(user_env = caller_env(2)) {
800+
lifecycle::deprecate_warn(
801+
when = "1.1.0",
802+
what = I("Using `c_across()` without supplying `cols`"),
803+
details = "Please supply `cols` instead.",
804+
user_env = user_env
805+
)
806+
}
807+
741808
df_unpack <- function(x, spec, caller_env, error_call = caller_env()) {
742809
size <- vec_size(x)
743810

R/conditions.R

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ needs_group_context <- function(cnd) {
6666
"dplyr:::mutate_mixed_null",
6767
"dplyr:::mutate_constant_recycle_error",
6868
"dplyr:::summarise_mixed_null",
69-
"dplyr:::error_pick_tidyselect"
69+
"dplyr:::error_pick_tidyselect",
70+
"dplyr:::warning_across_missing_cols_deprecated"
7071
))
7172
}
7273

@@ -368,11 +369,15 @@ signal_warnings <- function(state, error_call) {
368369
}
369370

370371
new_dplyr_warning <- function(data) {
371-
label <- cur_group_label(
372-
data$type,
373-
data$group_data$id,
374-
data$group_data$group
375-
)
372+
if (data$needs_group_context) {
373+
label <- cur_group_label(
374+
data$type,
375+
data$group_data$id,
376+
data$group_data$group
377+
)
378+
} else {
379+
label <- ""
380+
}
376381

377382
msg <- c(
378383
"i" = glue::glue("In argument `{data$name} = {data$expr}`."),

R/context.R

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,21 @@ stop_mask_type <- function(type) {
109109
}
110110

111111
cnd_data <- function(cnd, ctxt, mask_type, call) {
112+
needs_group_context <- needs_group_context(cnd)
113+
114+
if (needs_group_context) {
115+
group_data <- cur_group_data(mask_type)
116+
} else {
117+
group_data <- NULL
118+
}
119+
112120
list(
113121
cnd = cnd,
114122
name = ctxt$error_name,
115123
expr = ctxt$error_expression,
116124
type = mask_type,
117-
group_data = cur_group_data(mask_type),
125+
needs_group_context = needs_group_context,
126+
group_data = group_data,
118127
call = call
119128
)
120129
}

man/across.Rd

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/c_across.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)