When I intentionally enter list in the exec
function, the function doesn't give me an error.
It gave me the value returned based on the length of the list I provided.
exec(runif, list(min = -1, max = 100))
[1] 0.01183096 0.78551700
> exec(runif, list(min = -1, max = 100, n=5))
[1] 0.11955044 0.40972682 0.04771505
> exec(runif, !!!list(min = -1, max = 100, n=5))
[1] 7.474478 65.881655 58.168154 18.761874 91.956477
What does the exec function execute?
When I intentionally enter list in the exec
function, the function doesn't give me an error.
It gave me the value returned based on the length of the list I provided.
exec(runif, list(min = -1, max = 100))
[1] 0.01183096 0.78551700
> exec(runif, list(min = -1, max = 100, n=5))
[1] 0.11955044 0.40972682 0.04771505
> exec(runif, !!!list(min = -1, max = 100, n=5))
[1] 7.474478 65.881655 58.168154 18.761874 91.956477
What does the exec function execute?
Share Improve this question asked Mar 15 at 12:34 BreezeBreeze 3341 silver badge7 bronze badges 1- 1 if you don't have !!!, it doesn't call it properly – Coco Q. Commented Mar 15 at 12:54
3 Answers
Reset to default 3exec()
is the rlang
version of do.call()
and executes a function with the arguments provided. While do.call()
takes a single list of arguments, exec()
allows an arbitrary number of arguments via dynamic dots - this requires that lists of arguments be explicitly spliced by !!!
.
runif()
will return length(n)
observations if argument n
is not a single numeric value.
library(rlang)
# 2 observations returned because the list is length 2.
set.seed(0)
exec(runif, list(min = -1, max = 100)) #
[1] 0.8966972 0.2655087
set.seed(0)
# The call being constructed and executed by `exec()`:
runif(list(min = -1, max = 100)) # Equivalent to runif(2)
[1] 0.8966972 0.2655087
# 3 observations returned because the list is length 3.
set.seed(0)
exec(runif, list(min = -1, max = 100, n=5))
[1] 0.8966972 0.2655087 0.3721239
set.seed(0)
# The call being constructed and executed by `exec()`:
runif(list(min = -1, max = 100, n=5)) # Equivalent to runif(3)
[1] 0.8966972 0.2655087 0.3721239
# Arguments are spliced and used by `runif()`
set.seed(0)
exec(runif, !!!list(min = -1, max = 100, n=5))
[1] 89.77302 27.28536 37.84027 57.71248 90.91257
set.seed(0)
# The call being constructed and executed by `exec()`:
runif(min = 1, max = 100, n = 5)
[1] 89.77302 27.28536 37.84027 57.71248 90.91257
Ah I see @Iroha was faster than me. It is quite the same what I wrote.
rlang::exec()
takes in the first argument position a function - in this case runif
.
The !!!
in rlang
is a splice operator. It is the counter-operator/function of list()
.
R is actually a Lisp dialect. It is not compiling - but interpreter-only - however - its evaluation order allows some metaprogramming similar to (but not exactly to) how one knows it by Lisp languages.
(R was actually implemented first in a Lisp Dialect, Scheme - which is today's Racket).
R is a Lisp-1 like Scheme (while Common Lisp is Lisp-2. In Lisp-1 languages, the namespace of functions and variables/objects is the same - one. While in Lisp-2 languages functions have a separate namespace than normal variables or object names.
This has the peculiarity, that one can have a function and a variable with the same name. The interpreter/compiler can distinguish between them, because function names appear only in certain positions or are anyway designated by a leading #'
in Common Lisp.
One consequence that R is a Lisp language is - that there are actually no operators. Everything is a function or a special form.
Where you can see that?
You can in R write everything in function form - even operators.
1 + 2 + 3
you can write as a function call:
`+`(1, 2, 3)
Which in turn ist in Lisp actually:
(+ 1 2 3)
In R (like in all Lisp languages) operators are actually functions and one can always write them in the function call form.
I talked to about this, because rlang::exec()
has to do with putting together a function and its arguments and then executing this - so it is a typical meta-programming task typical for lisp languages.
It allows you to construct function calls in runtime.
And I also mention Lisp, because the !!!
in the rlang
package is actually the splice operator of Common Lisp or Scheme/Racket: @
.
Which is also a very typical metaprogramming tool in Lisp languages - to construct code (create code that creates code - and executes it).
rlang::exec(runif, !!!list(min = -1, max = 100, n=5))
evaluates actually first to:
rlang::exec(runif, min=-1, max=100, n=5)
rlang::exec
- when typing
>?rlang::exec
into the console, you get:
> ?rlang::exec
exec(.fn, ..., .env = caller_env())
Arguments:
.fn: A function, or function name as a string.
...: <dynamic> Arguments for ‘.fn’.
.env: Environment in which to evaluate the call. This will be most
useful if ‘.fn’ is a string, or the function has
side-effects.
Examples:
args <- list(x = c(1:10, 100, NA), na.rm = TRUE)
exec("mean", !!!args)
exec("mean", !!!args, trim = 0.2)
fs <- list(a = function() "a", b = function() "b")
lapply(fs, exec)
# Compare to do.call it will not automatically inline expressions
# into the evaluated call.
x <- 10
args <- exprs(x1 = x + 1, x2 = x * 2)
exec(list, !!!args)
do.call(list, args)
# exec() is not designed to generate pretty function calls. This is
# most easily seen if you call a function that captures the call:
f <- disp ~ cyl
exec("lm", f, data = mtcars)
# If you need finer control over the generated call, you'll need to
# construct it yourself. This may require creating a new environment
# with carefully constructed bindings
data_env <- env(data = mtcars)
eval(expr(lm(!!f, data)), data_env)
So the function signature is:
exec(.fn, ..., .env = caller_env())
The first position is a function .fn
.
the following positions ...
are collected to a list - the function argument list.
An explicit environment can be given as .env
- if not given (like it is here), then the calling environment of the rlang::exec()
function is taken as the evaluation environment of this call.
So all what exec
is collecting the ...
and apply .fn
on it.
So in this case it simply evaluates:
runif(min=-1, max=100, n=5)
It is in the end as if excatly this you are calling in that specific environment where rlang::exec()
is called.
Since n=5
you get a vector of the runif
so random uniform distribution with the minimal value of -1
and max value of 100
- 5
elements randomly selected.
So following this logic, the first expressions just evaluate exactly these expressions:
> runif(list(min=-1, max=100, n=5))
[1] 0.2814951 0.2275371 0.1372763
> runif(list(min=-1, max=100))
[1] 0.3214727 0.9828662
Let us regard >?runif
:
Uniform package:stats R Documentation
The Uniform Distribution
Description:
These functions provide information about the uniform distribution
on the interval from ‘min’ to ‘max’. ‘dunif’ gives the density,
‘punif’ gives the distribution function ‘qunif’ gives the quantile
function and ‘runif’ generates random deviates.
Usage:
dunif(x, min = 0, max = 1, log = FALSE)
punif(q, min = 0, max = 1, lower.tail = TRUE, log.p = FALSE)
qunif(p, min = 0, max = 1, lower.tail = TRUE, log.p = FALSE)
runif(n, min = 0, max = 1)
Arguments:
x, q: vector of quantiles.
p: vector of probabilities.
n: number of observations. If ‘length(n) > 1’, the length is
taken to be the number required.
min, max: lower and upper limits of the distribution. Must be finite.
log, log.p: logical; if TRUE, probabilities p are given as log(p).
lower.tail: logical; if TRUE (default), probabilities are P[X <= x],
otherwise, P[X > x].
Details:
If ‘min’ or ‘max’ are not specified they assume the default values
of ‘0’ and ‘1’ respectively.
The uniform distribution has density
f(x) = 1/(max-min)
for min <= x <= max.
For the case of u := min == max, the limit case of X == u is
assumed, although there is no density in that case and ‘dunif’
will return ‘NaN’ (the error condition).
‘runif’ will not generate either of the extreme values unless ‘max
= min’ or ‘max-min’ is small compared to ‘min’, and in particular
not for the default arguments.
Value:
‘dunif’ gives the density, ‘punif’ gives the distribution
function, ‘qunif’ gives the quantile function, and ‘runif’
generates random deviates.
The length of the result is determined by ‘n’ for ‘runif’, and is
the maximum of the lengths of the numerical arguments for the
other functions.
The numerical arguments other than ‘n’ are recycled to the length
of the result. Only the first elements of the logical arguments
are used.
Note:
The characteristics of output from pseudo-random number generators
(such as precision and periodicity) vary widely. See
‘.Random.seed’ for more information on R's random number
generation algorithms.
References:
Becker, R. A., Chambers, J. M. and Wilks, A. R. (1988) _The New S
Language_. Wadsworth & Brooks/Cole.
See Also:
‘RNG’ about random number generation in R.
Distributions for other standard distributions.
Examples:
u <- runif(20)
## The following relations always hold :
punif(u) == u
dunif(u) == 1
var(runif(10000)) #- ~ = 1/12 = .08333
It says, that if n
is an object, its length(n)
is taken.
So this is like as if you call:
runif(n=2)
runif(n=3)
so in this cases the min=
and max=
are ignored, since they are just part of the first list.
Confusing behavior of runif
I think this specific example is so confusing, because runif
has this peculiarity that its first argument's name is n
- as if an integer is expected - but it behaves so differently when an object like a list is given for n
- then it takes the length of that object.
With other functions - it would be much less confusing and clearer.
rlang::exec
differs from do.call
do.call
in R doesn't have the ...
of rlang::exec
.
It expects as the second argument a list of arguments.
> do.call(runif, list(min=-1, max=100, n=5))
[1] 41.181308 58.344790 39.653956 0.676207 60.094332
So it takes the list(min=-1, max=100, n=5)
and takes this list as the argument list of the function call.
So this evaluates to:
runif(min=-1, max=100, n=5)
instead to:
runif(list(min=-1, max=100, n=5))
exec
expects that separate arguments to the called function are passed to it. This is unlike do.call
which expects a list. If your arguments are already in a list then that list needs to be spliced into separate arguments in the exec
call using !!! .
The easiest way to see what is going on is to run a trace on runif
. This will show the actual call to runif
that is executed. Note that as shown in the args
output (and confirmed by looking at ?runif
) that runif
takes 1, 2, or 3 scalar arguments, n
, min
and max
. Now the first call to runif
below using exec
passes a list to runif
, not scalar arguments so the output is unspecified. Ditto for the second. Only the last run actually passes three scalar arguments.
Note that the output from trace
looks nicer if we use "runif"
in place of runif
in the exec
calls but I wanted to keep to the question.
library(rlang)
trace(runif)
args(runif)
## function (n, min = 0, max = 1)
## NULL
out1 <- exec(runif, list(min = -1, max = 100)) # list is passed
## trace: (function (n, min = 0, max = 1)
## .Call(C_runif, n, min, max))(list(min = -1, max = 100))
out2 <- exec(runif, list(min = -1, max = 100, n = 5)) # list is passed
## trace: (function (n, min = 0, max = 1)
## .Call(C_runif, n, min, max))(list(min = -1, max = 100, n = 5))
out3 <- exec(runif, !!!list(min = -1, max = 100, n = 5)) # separate args are passed
## trace: (function (n, min = 0, max = 1)
## .Call(C_runif, n, min, max))(min = -1, max = 100, n = 5)