Skip to content

Commit

Permalink
Merge pull request #607 from lionel-/fix-partial
Browse files Browse the repository at this point in the history
Review UI of partial()
  • Loading branch information
lionel- authored Dec 21, 2018
2 parents 69d2916 + f854cc6 commit 669726d
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 88 deletions.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ S3method(modify_if,double)
S3method(modify_if,integer)
S3method(modify_if,logical)
S3method(print,purrr_function_compose)
S3method(print,purrr_function_partial)
S3method(print,purrr_rate_backoff)
S3method(print,purrr_rate_delay)
S3method(rate_sleep,purrr_rate_backoff)
Expand Down
40 changes: 40 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@

## Life cycle

* We had to rename `...f` to `.f` in `partial()` in order to support
`... = ` argument (which would otherwise partial-match on
`...f`). This also makes `partial()` more consistent with other
purrr function signatures.

* The `.lazy` argument of `partial()` is soft-deprecated in favour of
quasiquotation.

* `%@%` is soft-deprecated, please use the operator exported in rlang
instead. The latter features an interface more consistent with `@`
as it uses NSE, supports S4 fields, and has an assignment variant.
Expand Down Expand Up @@ -155,6 +163,38 @@

## Minor improvements and fixes

* `partial()` now supports empty `... = ` argument to specify the
position of future arguments, relative to partialised ones. This
syntax is borrowed from (and implemented with) `rlang::call_modify()`.

To prevent partial matching of `...` on `...f`, the latter has been
renamed to `.f`, which is more consistent with other purrr function
signatures.

* `partial()` now supports quasiquotation. When you unquote an
argument, it is evaluated only once at function creation time. This
is more flexible than the `.lazy` argument since you can control the
timing of evaluation for each argument. Consequently, `.lazy` is
soft-deprecated (#457).

* Fixed an infinite loop when partialised function is given the same
name as the original function (#387).

* `partial()` now calls `as_closure()` on primitive functions to
ensure argument matching (#360).

* The `.lazy` argument of `partial()` is soft-deprecated in favour of
quasiquotation:

```r
# Before
partial(fn, u = runif(1), n = rnorm(1), .lazy = FALSE)

# After
partial(fn, u = !!runif(1), n = !!rnorm(1)) # All constant
partial(fn, u = !!runif(1), n = rnorm(1)) # First constant
```

* New `rate_backoff()` and `rate_delay()` functions to create rate
objects. You can pass rates to `insistently()`, `slowly()`, or the
lower level function `rate_sleep()`. This will cause a function to
Expand Down
155 changes: 115 additions & 40 deletions R/partial.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@
#' some of the arguments. It is particularly useful in conjunction with
#' functionals and other function operators.
#'
#' @section Design choices:
#' @param .f a function. For the output source to read well, this should be a
#' named function.
#' @param ... named arguments to `.f` that should be partially applied.
#'
#' There are many ways to implement partial function application in R.
#' (see e.g. `dots` in \url{https://github.com/crowding/ptools} for another
#' approach.) This implementation is based on creating functions that are as
#' similar as possible to the anonymous functions that you'd create by hand,
#' if you weren't using `partial`.
#' Pass an empty `... = ` argument to specify the position of future
#' arguments relative to partialised ones. See
#' [rlang::call_modify()] to learn more about this syntax.
#'
#' These dots support quasiquotation and quosures. If you unquote a
#' value, it is evaluated only once at function creation time.
#' Otherwise, it is evaluated each time the function is called.
#' @param .env Soft-deprecated as of purrr 0.3.0. The environments are
#' now captured via quosures.
#' @param .first Soft-deprecated as of purrr 0.3.0. Please pass an
#' empty argument `... = ` to specify the position of future
#' arguments.
#' @param .lazy Soft-deprecated as of purrr 0.3.0. Please unquote the
#' arguments that should be evaluated once at function creation time.
#'
#' @param ...f a function. For the output source to read well, this should be a
#' named function.
#' @param ... named arguments to `...f` that should be partially applied.
#' @param .env the environment of the created function. Defaults to
#' [parent.frame()] and you should rarely need to modify this.
#' @param .lazy If `TRUE` arguments evaluated lazily, if `FALSE`,
#' evaluated when `partial` is called.
#' @param .first If `TRUE`, the partialized arguments are placed
#' to the front of the function signature. If `FALSE`, they are
#' moved to the back. Only useful to control position matching of
#' arguments when the partialized arguments are not named.
#' @export
#' @examples
#' # Partial is designed to replace the use of anonymous functions for
#' # filling in function arguments. Instead of:
Expand All @@ -43,8 +42,9 @@
#' f()
#' f()
#'
#' # You can override this by saying .lazy = FALSE
#' f <- partial(runif, n = rpois(1, 5), .lazy = FALSE)
#' # If you unquote an argument, it is evaluated only once at function
#' # creation time:
#' f <- partial(runif, n = !!rpois(1, 5))
#' f
#' f()
#' f()
Expand All @@ -55,32 +55,107 @@
#' plot2 <- partial(plot, my_long_variable)
#' plot2()
#' plot2(runif(10), type = "l")
partial <- function(...f, ..., .env = parent.frame(), .lazy = TRUE,
.first = TRUE) {
stopifnot(is.function(...f))
#'
#'
#' # By default, partialised arguments are passed before new ones:
#' my_list <- partial(list, 1, 2)
#' my_list("foo")
#'
#' # Control the position of these arguments by passing an empty
#' # `... = ` argument:
#' my_list <- partial(list, 1, ... = , 2)
#' my_list("foo")
#' @export
partial <- function(.f,
...,
.env = NULL,
.lazy = NULL,
.first = NULL) {
args <- enquos(...)
if (has_name(args, "...f")) {
stop_defunct("`...f` has been renamed to `.f` as of purrr 0.3.0.")
}

if (.lazy) {
fcall <- substitute(...f(...))
} else {
fcall <- make_call(substitute(...f), .args = list(...))
fn_expr <- enexpr(.f)
fn <- switch(typeof(.f),
builtin = ,
special =
as_closure(.f),
closure =
.f,
abort(sprintf("`.f` must be a function, not a %s", friendly_type_of(.f)))
)

if (!is_null(.env)) {
signal_soft_deprecated(paste_line(
"The `.env` argument is soft-deprecated as of purrr 0.3.0.",
))
}
if (!is_null(.lazy)) {
signal_soft_deprecated(paste_line(
"The `.lazy` argument is soft-deprecated as of purrr 0.3.0.",
"Please unquote the arguments that should be evaluated once and for all.",
"",
" # Before:",
" partial(fn, u = runif(1), n = rnorm(1), .lazy = FALSE)",
"",
" # After:",
" partial(fn, u = !!runif(1), n = !!rnorm(1)) # All constant",
" partial(fn, u = !!runif(1), n = rnorm(1)) # First constant"
))
if (!.lazy) {
args <- map(args, eval_tidy, env = caller_env())
}
}
if (!is_null(.first)) {
signal_soft_deprecated(paste_line(
"The `.first` argument is soft-deprecated as of purrr 0.3.0.",
"Please pass a `... =` argument instead.",
"",
" # Before:",
" partial(fn, x = 1, y = 2, .first = FALSE)",
"",
" # After:",
" partial(fn, x = 1, y = 2, ... = ) # Partialised arguments last",
" partial(fn, x = 1, ... = , y = 2) # Partialised arguments around"
))
}

# Pass on ... from parent function
n <- length(fcall)
if (!.first && n > 1) {
tmp <- fcall[1]
tmp[[2]] <- quote(...)
tmp[seq(3, n + 1)] <- fcall[seq(2, n)]
names(tmp)[seq(3, n + 1)] <- names2(fcall)[seq(2, n)]
fcall <- tmp
if (is_false(.first)) {
# For compatibility
call <- call_modify(call2(fn), ... = , !!!args)
} else {
fcall[[n + 1]] <- quote(...)
# Pass on `...` from parent function. It should be last, this way if
# `args` also contain a `...` argument, the position in `args`
# prevails.
call <- call_modify(call2(fn), !!!args, ... = )
}

partialised <- function(...) {
eval_tidy(call)
}

args <- list("..." = quote(expr = ))
new_function(args, fcall, .env)
structure(
partialised,
class = c("purrr_function_partial", "function"),
body = call,
fn = fn_expr
)
}

make_call <- function(f, ..., .args = list()) {
as.call(c(f, ..., .args))
#' @export
print.purrr_function_partial <- function(x, ...) {
cat("<partialised>\n")

body <- quo_squash(partialised_body(x))
body[[1]] <- partialised_fn(x)
body(x) <- body

# Remove reference to internal environment
x <- set_env(x, global_env())

print(x, ...)
}

partialised_body <- function(x) attr(x, "body")
partialised_fn <- function(x) attr(x, "fn")
55 changes: 31 additions & 24 deletions man/partial.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests/testthat/test-partial-print.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<partialised>
function (...)
foo(y = 3, ...)
Loading

0 comments on commit 669726d

Please sign in to comment.