#' Dynamic dots features
#'
#' @description
#'
#' The base `...` syntax supports:
#'
#' - __Forwarding__ arguments from function to function, matching them
#' along the way to arguments.
#'
#' - __Collecting__ arguments inside data structures, e.g. with [c()] or
#' [list()].
#'
#' Dynamic dots offer a few additional features,
#' [injection][topic-inject] in particular:
#'
#' 1. You can __splice__ arguments saved in a list with the splice
#' operator [`!!!`][splice-operator].
#'
#' 2. You can __inject__ names with [glue syntax][glue-operators] on
#' the left-hand side of `:=`.
#'
#' 3. Trailing commas are ignored, making it easier to copy and paste
#' lines of arguments.
#'
#'
#' @section Add dynamic dots support in your functions:
#'
#' If your function takes dots, adding support for dynamic features is
#' as easy as collecting the dots with [list2()] instead of [list()].
#' See also [dots_list()], which offers more control over the collection.
#'
#' In general, passing `...` to a function that supports dynamic dots
#' causes your function to inherit the dynamic behaviour.
#'
#' In packages, document dynamic dots with this standard tag:
#'
#' ```
#' @@param ... <[`dynamic-dots`][rlang::dyn-dots]> What these dots do.
#' ```
#'
#' @name dyn-dots
#' @aliases tidy-dots doc_dots_dynamic
#'
#' @examples
#' f <- function(...) {
#' out <- list2(...)
#' rev(out)
#' }
#'
#' # Trailing commas are ignored
#' f(this = "that", )
#'
#' # Splice lists of arguments with `!!!`
#' x <- list(alpha = "first", omega = "last")
#' f(!!!x)
#'
#' # Inject a name using glue syntax
#' if (is_installed("glue")) {
#' nm <- "key"
#' f("{nm}" := "value")
#' f("prefix_{nm}" := "value")
#' }
NULL
#' @rdname dyn-dots
#' @usage NULL
#' @export
`:=` <- function(x, y) {
abort("`:=` can only be used within dynamic dots.", call = caller_env())
}
#' Collect dynamic dots in a list
#'
#' `list2(...)` is equivalent to `list(...)` with a few additional
#' features, collectively called [dynamic dots][dyn-dots]. While
#' `list2()` hard-code these features, `dots_list()` is a lower-level
#' version that offers more control.
#'
#' @param ... Arguments to collect in a list. These dots are
#' [dynamic][dyn-dots].
#' @return A list containing the `...` inputs.
#'
#' @details
#' For historical reasons, `dots_list()` creates a named list by
#' default. By comparison `list2()` implements the preferred behaviour
#' of only creating a names vector when a name is supplied.
#'
#' @export
list2 <- function(...) {
.Call(
ffi_dots_list,
frame_env = environment(),
named = NULL,
ignore_empty = "trailing",
preserve_empty = FALSE,
unquote_names = TRUE,
homonyms = "keep",
check_assign = FALSE
)
}
#' @rdname list2
#' @usage NULL
#' @export
ll <- list2
# Preserves empty arguments
list3 <- function(...) {
.Call(
ffi_dots_list,
frame_env = environment(),
named = NULL,
ignore_empty = "trailing",
preserve_empty = TRUE,
unquote_names = TRUE,
homonyms = "keep",
check_assign = FALSE
)
}
#' @rdname list2
#' @param .named If `TRUE`, unnamed inputs are automatically named
#' with [as_label()]. This is equivalent to applying
#' [exprs_auto_name()] on the result. If `FALSE`, unnamed elements
#' are left as is and, if fully unnamed, the list is given minimal
#' names (a vector of `""`). If `NULL`, fully unnamed results are
#' left with `NULL` names.
#' @param .ignore_empty Whether to ignore empty arguments. Can be one
#' of `"trailing"`, `"none"`, `"all"`. If `"trailing"`, only the
#' last argument is ignored if it is empty.
#' @param .preserve_empty Whether to preserve the empty arguments that
#' were not ignored. If `TRUE`, empty arguments are stored with
#' [missing_arg()] values. If `FALSE` (the default) an error is
#' thrown when an empty argument is detected.
#' @param .homonyms How to treat arguments with the same name. The
#' default, `"keep"`, preserves these arguments. Set `.homonyms` to
#' `"first"` to only keep the first occurrences, to `"last"` to keep
#' the last occurrences, and to `"error"` to raise an informative
#' error and indicate what arguments have duplicated names.
#' @param .check_assign Whether to check for `<-` calls. When `TRUE` a
#' warning recommends users to use `=` if they meant to match a
#' function parameter or wrap the `<-` call in curly braces otherwise.
#' This ensures assignments are explicit.
#' @export
#' @examples
#' # Let's create a function that takes a variable number of arguments:
#' numeric <- function(...) {
#' dots <- list2(...)
#' num <- as.numeric(dots)
#' set_names(num, names(dots))
#' }
#' numeric(1, 2, 3)
#'
#' # The main difference with list(...) is that list2(...) enables
#' # the `!!!` syntax to splice lists:
#' x <- list(2, 3)
#' numeric(1, !!! x, 4)
#'
#' # As well as unquoting of names:
#' nm <- "yup!"
#' numeric(!!nm := 1)
#'
#'
#' # One useful application of splicing is to work around exact and
#' # partial matching of arguments. Let's create a function taking
#' # named arguments and dots:
#' fn <- function(data, ...) {
#' list2(...)
#' }
#'
#' # You normally cannot pass an argument named `data` through the dots
#' # as it will match `fn`'s `data` argument. The splicing syntax
#' # provides a workaround:
#' fn("wrong!", data = letters) # exact matching of `data`
#' fn("wrong!", dat = letters) # partial matching of `data`
#' fn(some_data, !!!list(data = letters)) # no matching
#'
#' # Empty trailing arguments are allowed:
#' list2(1, )
#'
#' # But non-trailing empty arguments cause an error:
#' try(list2(1, , ))
#'
#' # Use the more configurable `dots_list()` function to preserve all
#' # empty arguments:
#' list3 <- function(...) dots_list(..., .preserve_empty = TRUE)
#'
#' # Note how the last empty argument is still ignored because
#' # `.ignore_empty` defaults to "trailing":
#' list3(1, , )
#'
#' # The list with preserved empty arguments is equivalent to:
#' list(1, missing_arg())
#'
#'
#' # Arguments with duplicated names are kept by default:
#' list2(a = 1, a = 2, b = 3, b = 4, 5, 6)
#'
#' # Use the `.homonyms` argument to keep only the first of these:
#' dots_list(a = 1, a = 2, b = 3, b = 4, 5, 6, .homonyms = "first")
#'
#' # Or the last:
#' dots_list(a = 1, a = 2, b = 3, b = 4, 5, 6, .homonyms = "last")
#'
#' # Or raise an informative error:
#' try(dots_list(a = 1, a = 2, b = 3, b = 4, 5, 6, .homonyms = "error"))
#'
#'
#' # dots_list() can be configured to warn when a `<-` call is
#' # detected:
#' my_list <- function(...) dots_list(..., .check_assign = TRUE)
#' my_list(a <- 1)
#'
#' # There is no warning if the assignment is wrapped in braces.
#' # This requires users to be explicit about their intent:
#' my_list({ a <- 1 })
dots_list <- function(...,
.named = FALSE,
.ignore_empty = c("trailing", "none", "all"),
.preserve_empty = FALSE,
.homonyms = c("keep", "first", "last", "error"),
.check_assign = FALSE) {
.Call(
ffi_dots_list,
frame_env = environment(),
named = .named,
ignore_empty = .ignore_empty,
preserve_empty = .preserve_empty,
unquote_names = TRUE,
homonyms = .homonyms,
check_assign = .check_assign
)
}
dots_split <- function(...,
.n_unnamed = NULL,
.ignore_empty = c("trailing", "none", "all"),
.preserve_empty = FALSE,
.homonyms = c("keep", "first", "last", "error"),
.check_assign = FALSE) {
dots <- .Call(
ffi_dots_list,
frame_env = environment(),
named = NULL,
ignore_empty = .ignore_empty,
preserve_empty = .preserve_empty,
unquote_names = TRUE,
homonyms = .homonyms,
check_assign = .check_assign
)
if (is_null(names(dots))) {
if (length(dots)) {
unnamed_idx <- TRUE
} else {
unnamed_idx <- lgl()
}
n <- length(dots)
} else {
unnamed_idx <- names(dots) == ""
n <- sum(unnamed_idx)
}
if (!is_null(.n_unnamed) && all(n != .n_unnamed)) {
ns <- oxford_comma(.n_unnamed)
abort(sprintf("Expected %s unnamed arguments in `...`", ns))
}
unnamed <- dots[unnamed_idx]
named <- dots[!unnamed_idx]
# Remove empty names vector
names(unnamed) <- NULL
list(named = named, unnamed = unnamed)
}
#' Splice values at dots collection time
#'
#' @description
#' The splicing operator `!!!` operates both in values contexts like
#' [list2()] and [dots_list()], and in metaprogramming contexts like
#' [expr()], [enquos()], or [inject()]. While the end result looks the
#' same, the implementation is different and much more efficient in
#' the value cases. This difference in implementation may cause
#' performance issues for instance when going from:
#'
#' ```r
#' xs <- list(2, 3)
#' list2(1, !!!xs, 4)
#' ```
#'
#' to:
#'
#' ```r
#' inject(list2(1, !!!xs, 4))
#' ```
#'
#' In the former case, the performant value-splicing is used. In the
#' latter case, the slow metaprogramming splicing is used.
#'
#' A common practical case where this may occur is when code is
#' wrapped inside a tidyeval context like `dplyr::mutate()`. In this
#' case, the metaprogramming operator `!!!` will take over the
#' value-splicing operator, causing an unexpected slowdown.
#'
#' To avoid this in performance-critical code, use `splice()` instead
#' of `!!!`:
#'
#' ```r
#' # These both use the fast splicing:
#' list2(1, splice(xs), 4)
#' inject(list2(1, splice(xs), 4))
#' ```
#'
#' @param x A list or vector to splice non-eagerly.
#' @export
splice <- function(x) {
.Call(ffi_new_splice_box, x)
}
#' @rdname splice
#' @export
is_spliced <- function(x) {
.Call(ffi_is_splice_box, x)
}
#' @rdname splice
#' @export
is_spliced_bare <- function(x) {
is_bare_list(x) || is_spliced(x)
}
#' @export
print.rlang_box_splice <- function(x, ...) {
cat_line("<spliced>")
print(unbox(x))
}
#' Evaluate dots with preliminary splicing
#'
#' This is a tool for advanced users. It captures dots, processes
#' unquoting and splicing operators, and evaluates them. Unlike
#' [dots_list()], it does not flatten spliced objects, instead they
#' are attributed a `spliced` class (see [splice()]). You can process
#' spliced objects manually, perhaps with a custom predicate (see
#' [flatten_if()]).
#'
#' @inheritParams dots_list
#' @param ... Arguments to evaluate and process splicing operators.
#'
#' @keywords internal
#' @export
#' @examples
#' dots <- dots_values(!!! list(1, 2), 3)
#' dots
#'
#' # Flatten the objects marked as spliced:
#' flatten_if(dots, is_spliced)
dots_values <- function(...,
.ignore_empty = c("trailing", "none", "all"),
.preserve_empty = FALSE,
.homonyms = c("keep", "first", "last", "error"),
.check_assign = FALSE) {
.External(
ffi_dots_values,
env = environment(),
named = NULL,
ignore_empty = .ignore_empty,
preserve_empty = .preserve_empty,
unquote_names = TRUE,
homonyms = .homonyms,
check_assign = .check_assign
)
}
# Micro optimisation: Inline character vectors in formals list
formals(dots_values) <- pairlist(
... = quote(expr = ),
.ignore_empty = c("trailing", "none", "all"),
.preserve_empty = FALSE,
.homonyms = c("keep", "first", "last", "error"),
.check_assign = FALSE
)
#' How many arguments are currently forwarded in dots?
#'
#' This returns the number of arguments currently forwarded in `...`
#' as an integer.
#'
#' @param ... Forwarded arguments.
#' @keywords internal
#' @export
#' @examples
#' fn <- function(...) dots_n(..., baz)
#' fn(foo, bar)
dots_n <- function(...) {
nargs()
}
abort_dots_homonyms <- function(dots, dups) {
.__error_call__. <- "caller"
nms <- names(dots)
# This includes the first occurrence as well
dups_all <- nms %in% nms[dups]
dups_nms <- unique(nms[dups_all])
dups_n <- length(dups_nms)
if (!dups_n) {
abort("Internal error: Expected dots duplicates")
}
enums <- map(dups_nms, homonym_enum, dups_all, nms)
line <- "Multiple arguments named `%s` at positions %s."
enums_lines <- map2_chr(dups_nms, enums, sprintf, fmt = line)
abort(c(
"Arguments in `...` must have unique names.",
set_names(enums_lines, "x")
))
}
homonym_enum <- function(nm, dups, nms) {
dups[nms != nm] <- FALSE
oxford_comma(as.character(which(dups)), final = "and")
}
# This helper is used when splicing S3 or S4 objects found
# in `!!!`. It is similar to `as.list()`, but the names of
# `x` always end up on the names of the output list,
# unlike `as.list.factor()`.
rlang_as_list <- function(x) {
if ("list" %in% class(x)) {
# `x` explicitly inherits from `"list"`, which we take it to mean
# that it has list storage (i.e. it's not a class like POSIXlt,
# it's not proxied, and it's not a scalar object like `"lm"`)
out <- vec_unstructure(x)
} else if (is.list(x)) {
out <- rlang_as_list_from_list_impl(x)
} else {
out <- rlang_as_list_impl(x)
}
names(out) <- names(x)
out
}
rlang_as_list_impl <- function(x) {
n <- length(x)
out <- vector("list", n)
for (i in seq_len(n)) {
out[[i]] <- x[[i]]
}
out
}
# Special handling if `x` is already a list.
# This avoids the potential for `out[[i]] <- NULL`,
# which shortens the list.
rlang_as_list_from_list_impl <- function(x) {
n <- length(x)
out <- vector("list", n)
for (i in seq_len(n)) {
elt <- x[[i]]
if (is.null(elt)) {
next
}
out[[i]] <- elt
}
out
}
#' Development notes - `dots.R`
#'
#' @section `.__error_call__.` flag in dots collectors:
#'
#' Dots collectors like [dots_list()] are a little tricky because they
#' may error out in different situations. Do we want to forward the
#' context, i.e. set the call flag to the calling environment?
#' Collectors throw errors in these cases:
#'
#' 1. While checking their own parameters, in which case the relevant
#' context is the collector itself and we don't forward.
#'
#' 2. While collecting the dots, during evaluation of the supplied
#' arguments. In this case forwarding or not is irrelevant because
#' expressions in `...` are evaluated in their own environment
#' which is not connected to the collector's context.
#'
#' 3. While collecting the dots, during argument constraints checks
#' such as determined by the `.homonyms` argument. In this case we
#' want to forward the context because the caller of the dots
#' collector is the one who determines the constraints for its
#' users.
#'
#' @keywords internal
#' @name dev-notes-dots
NULL
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.