library(adorn)
By default a decorated function keeps the formal arguments of the original, so
formals(), argument matching, defaults and missing() all behave as before.
That is what you want for timed or retry, but it rules out decorators whose
whole point is to change the signature: fixing some arguments in advance
(partial application), currying, or adding an argument.
A factory-style decorator can opt in with formals = "replace". The decorated
function then takes its formals from the function the factory returns, instead
of from f:
two_to := decorator(
function(f) function(y) f(2, y),
formals = "replace"
)
pow := two_to %@% function(x, y) x^y
formals(pow)
#> $y
#>
#>
pow(3)
#> [1] 8
The examples use :=, which adorn re-exports from S7. It assigns the result and
passes the variable’s name as name: logged := decorator(...) names the
decorator, and pow := logged %@% f names the function, which is what
decorator_context()$name reports even for an anonymous f.
adorn ships no partial-application decorator. This vignette builds one, and a few others, with only the public API.
With the default, formals = "keep", adorn guarantees that argument matching
and missing() work exactly as they do for the original. With "replace",
that guarantee passes to you: the decorated function behaves like
the function you return. Three things stay the same:
f inside the factory still has the original signature, so you can read
formals(f) to decide what to build.undecorate() returns the original function with its full formals.decorators() and decorator_context() work as usual.It only applies to factory style. A call-style wrapper is fixed to
function(f, ..., params) and has no returned function, so decorator()
refuses the combination.
partial fixes some arguments by name and leaves the rest. It reads the
formals of f, drops the fixed ones, and builds a function with what is left
that calls f with the fixed values plus the remaining arguments forwarded.
partial := decorator(
function(f, .fixed = list()) {
fmls <- formals(f)
unknown <- setdiff(names(.fixed), names(fmls))
if (length(unknown) > 0L && !"..." %in% names(fmls)) {
stop("Not arguments of `f`: ", paste(unknown, collapse = ", "), call. = FALSE)
}
keep <- fmls[setdiff(names(fmls), names(.fixed))]
# Forward each remaining argument by name; `...` is forwarded unnamed.
fwd <- lapply(names(keep), as.name)
names(fwd) <- ifelse(names(keep) == "...", "", names(keep))
# Quote symbols and calls so they are passed as values, not evaluated.
fixed <- lapply(.fixed, function(v) {
if (is.language(v)) call("quote", v) else v
})
as.function(c(keep, list(as.call(c(list(f), fixed, fwd)))))
},
formals = "replace"
)
pow <- function(x, y) x^y
two_to := partial(.fixed = list(x = 2)) %@% pow
square := partial(.fixed = list(y = 2)) %@% pow
names(formals(two_to))
#> [1] "y"
two_to(3)
#> [1] 8
square(4)
#> [1] 16
Fixed arguments can be leading or trailing, and the remaining arguments keep
their defaults and missing() behaviour:
describe <- function(x, sep = "-", suffix) {
paste0(x, sep, if (missing(suffix)) "none" else suffix)
}
tagged := partial(.fixed = list(x = "id")) %@% describe
tagged()
#> [1] "id-none"
tagged(suffix = "7")
#> [1] "id-7"
If f has ..., a fixed name that is not a formal is passed on through the
dots, which is handy for forwarding options to the function the dots go to.
Without ... it is an error, because it is almost certainly a typo:
joined := partial(.fixed = list(sep = "-")) %@% function(...) paste(...)
joined("a", "b")
#> [1] "a-b"
partial(.fixed = list(z = 1)) %@% pow
#> Error: Not arguments of `f`: z
Every layer sees the signature of the layer below it, so partial stacks with
any other decorator in either order:
logged := decorator(function(f, ...) {
cat("calling with", length(list(...)), "argument(s)\n")
f(...)
})
a := logged %@% partial(.fixed = list(x = 2)) %@% pow
b := partial(.fixed = list(x = 2)) %@% logged %@% pow
a(3)
#> calling with 1 argument(s)
#> [1] 8
b(3)
#> calling with 2 argument(s)
#> [1] 8
decorators(a)
#> [1] "logged" "partial"
decorators(b)
#> [1] "partial" "logged"
In a, logged sees the reduced signature and one argument. In b, logged
is applied to the full pow, and partial is applied on top.
undecorate() still gives back the original, with all its arguments:
names(formals(undecorate(a)))
#> [1] "x" "y"
A decorator can return any signature it likes. curry turns a function of
several arguments into a chain of one-argument functions, each returning the
next, until it has them all. Each step is given the real name of the argument
it collects, so the formals stay meaningful at every stage:
curry := decorator(
function(f) {
# `f` still has the original formals here, so they say what to collect.
args <- setdiff(names(formals(f)), "...")
step <- function(done) {
arg <- args[[length(done) + 1L]]
last <- length(done) + 1L == length(args)
next_fn <- if (last) function(all) do.call(f, all) else step
# The body calls `next_fn` with everything collected so far plus this
# argument. Both are embedded as values, not looked up by name, so an
# argument called `done` or `f` cannot shadow anything.
value <- as.call(c(list(as.name("list")), setNames(list(as.name(arg)), arg)))
body <- as.call(list(next_fn, call("c", done, value)))
as.function(c(setNames(alist(x = ), arg), list(body)), envir = baseenv())
}
step(list())
},
formals = "replace"
)
box <- function(width, height, depth) width * height * depth
volume := curry %@% box
volume(2)(3)(4)
#> [1] 24
The decorated function takes only the first argument, and each step takes the next one:
names(formals(volume))
#> [1] "width"
names(formals(volume(2)))
#> [1] "height"
A partly applied curry is an ordinary function, so it can be reused:
two_wide <- volume(2)
two_wide(3)(4)
#> [1] 24
two_wide(5)(5)
#> [1] 50
curry stacks like any other decorator. Here partial fixes depth first, so
only width and height are left to collect:
area_of_slab := curry %@% partial(.fixed = list(depth = 10)) %@% box
names(formals(area_of_slab))
#> [1] "width"
area_of_slab(2)(3)
#> [1] 60
This curry always collects every argument, so defaults on the original are
not used. Fix an argument with partial first if you want a default.
The returned function may have more arguments than f, too:
with_scale := decorator(
function(f) function(x, scale = 10) f(x) * scale,
formals = "replace"
)
double_it := with_scale %@% function(x) x
double_it(2)
#> [1] 20
double_it(2, scale = 1)
#> [1] 2
formals = "replace" works with decorate() for plain functions and R6
methods, where the method’s signature is free:
library(R6)
Account <- R6Class("Account",
public = list(
balance = 100,
deposit = function(amount, fee) self$balance + amount - fee
)
)
decorate("deposit", partial(.fixed = list(fee = 1)), class = Account)
Account$new()$deposit(50)
#> [1] 149
Take care with S3, S4 and S7 methods. They have to match the signature of
their generic, so fixing a leading argument breaks dispatch, and S7 checks the
method’s formals against the generic’s when you register it. Fixing only
trailing arguments, which the generic does not use, is the safe case. See
vignette("classes").
formals = "replace" changes nothing about how a decorated call is made. The
extra work is done once, when the function is decorated. A call to a partial
adds one function call: the one your returned function makes to f.