I have been an enthusiastic user of R's purrr
package for quite a while and recently ran into a question regarding purrr::partial
. Suppose I define a two-argument function
f <- function(x, y) x + y
and partialize it by setting the y
argument to some global variable's value:
yy <- 1
fp <- partial(f, y = !!yy)
fp(3) # 3 + 1 = 4
Unquoting yy
(that is, using y = !!yy
as opposed to y = yy
) causes yy
to be evaluated only once when fp
is created; in particular, modifying yy
after this step does not alter fp
:
yy <- 2
fp(3) # still: 3 + 1 = 4
Here is my question: What exactly does partial
do after evaluating yy
? -- I see two possibilities:
yy
is "hard-wired" into the body of fp
, meaning that it is not passed as an argument when fp
is called.yy
is more or less treated as if it was the y
argument's default value (without the option of overriding the default), meaning that fp
internally calls f
(or a copy of it) to which the value of yy
is silently passed as an argument matched with y
. In this case fp
is no more than a syntactic wrapper around f
.Trying to explore the second possibility I modfied the definition of f
after defining fp
. This does not alter fp
, meaning that fp
does not contain any outside reference to f
; however, this does not rule out the (theoretical) possibility of fp
containing a copy of the old version of f
. (Conclusion: This approach does not help.)
Some practical background to motivate my question: In my current project I have defined lots of functions that use (a) arguments varying from call to call, (b) arguments representing "configuration data" or "domain knowledge". The data matched with the (b) arguments (which may be considerable amounts of data) do not change from call to call, but may change when I commit an update; in any case I believe this data should not be hard-coded in my functions. My strategy is to read the configuration data from some files at start-up time and integrate it into my functions by partializing the arguments in (b). Applying the partialized functions via purrr::pmap
to some tibbles turned out to be kind of slow, which made me suspect that the configuration data may still be passed when the function is called -- hence my question. (If anyone has some thoughts on the "partialization strategy" briefly described above, I will be keenly interested in these, too.)
It seems like it is option 2. Try:
f <- function(x, y) x + y
yy <- 5
fp1 <- partial(f, y = !! yy)
debugonce(f)
fp1(3)
Here you can see that, if in RStudio, the debugger will open the original function f
to which the arguments x = 3
and y = 5
are passed. However, the partialized function is not calling the real function f
but a quoted copy of it. If you change f
after it has been partialzed, the debugger won't find it anymore.
f <- function(x, y) x + y
yy <- 5
fp1 <- partial(f, y = !! yy)
f <- function(x, y) x + 2 * y
debugonce(f)
fp1(3) # debugger will not open
It is possible to mimic the behavior of partial
by constructing the function to partialize yourself. However, in this case neither f
nor yy
are captured so changing them will effect the output of your partialized function:
f <- function(x, y) x + y
yy <- 5
# similar to `partial` but captures neither `f` nor `yy`
fp2 <- function(x) f(x, yy)
fp2(3)
#> [1] 8
# so if yy changes, so will the output of fp2
yy <- 10
fp2(3)
#> [1] 13
# and if f changes, so will the output of fp2
f <- function(x, y) x + 2 * y
fp2(3)
#> [1] 23
Created on 2020-07-13 by the reprex package (v0.3.0)
To better understand how partial
is working we can construct a simple_partial
function the following way:
library(rlang)
f <- function(x, y) x + y
yy <- 5
simple_partial <- function(.f, ...) {
# capture arguments
args <- enquos(...)
# capture function
fn_expr <- enexpr(.f)
# construct call with function and supplied arguments
# in the ... go all arguments which will be supplied later
call <- call_modify(call2(.f), !!! args, ... = )
# turn call into a quosure (= expr and environment where it should be evaluated)
call <- new_quosure(call, caller_env())
# create child environment of current environment and turn it into a data mask
mask <- new_data_mask(env())
# return this function
function(...) {
# bind the ... from current environment to the data mask
env_bind(mask, ... = env_get(current_env(), "..."))
# evaluate the quoted call in the data mask where all additional values can be found
eval_tidy(call, mask)
}
}
fp3 <- simple_partial(f, y = !! yy)
fp3(1)
#> [1] 6
Created on 2020-07-13 by the reprex package (v0.3.0)