Typically when writing an R function you include any arguments to the function, and default values if applicable, in the definition like so:
Until recently I had never considered how one might add arguments to a function
after it’s defined. Given a function like the one below, how would we add the
arguments x
and y
?
You can reassign the formal arguments of a function using formals()
, though as
the documentation notes “this is advanced, dangerous coding”. I will take this
moment to note that re-defining functions in this way is probably not something
you generally should need to do very often. But for various reasons that are
outside the scope of this post, I did. Here’s an example: we create a list of
argument names and default values, and assign it to formals(add)
.
alist()
is a special function for creating lists of function arguments. Unlike
list()
it allows empty arguments.
alist()
is useful for creating function arguments with no defaults.
Again, this is a pretty peculiar way to define a function. My situation was a
bit more complicated though: I needed to be able to generate the list of
arguments given their names, and do this for many functions. Constructing the
arguments by hand with alist()
was out, so how do we construct a list of
arguments given a list of their names? Of course we need some information about
whether the arguments should have default values or not. If the arguments all
have defaults, and we know what they are, then we can create a list of the
defaults, assign the argument names as the names of the list elements, and
assign that list to formals(add)
. But what if we don’t want the arguments to
have default values?
To understand how to do this requires a brief detour into symbols. Symbols in R (also called names) represent the name of an object. I’m not going to go into too much detail on symbols, but you should check out the chapter on Expressions in Hadley Wickham’s Advanced R book if you want to learn more about them. The important thing for this post is that there is a special symbol, the empty symbol, that is a sort of weird void of nothing* that represents missing arguments.
You can create an empty symbol in base R with the very weird looking expression
quote(expr = )
, or using the rlang
package with missing_arg()
.
Empty symbols cannot be assigned to variables! 😱
However, you can store an empty symbol inside another data structure, like a list.
Back to the problem at hand: we need to create a list of arguments based on
their names, and store an empty symbol as the values. First we’ll create an
empty list of NULL
s, then we’ll use purrr::map()
to assign an empty symbol
to each element of the list.
Then we can assign this list to the formal arguments of the function and view the function definition.
Now, what if our list needs to contain a mix of arguments with defaults and arguments without? This is a bit more tricky. Let’s say for this example we have argument names and default values stored separately. There may be more names than defaults, in which case we want to match the defaults to the names from the last argument backward, with any other remaining names having no default.
I’ll add a third argument, z
to our add()
function. z
has a default value
of 1, and the other arguments have no defaults. First we create a list based on
the names of the arguments.
Then add the defaults to the last elements of the list. This is a bit more code than is strictly needed, but it will come in handy later:
To assign some other value to the first two elements of the list, I would
normally do something like x[1:2] <- "some_value"
, but this does not work with
empty symbols:
The final solution ended up being rather more complicated than I’d hoped, and involved a for loop where I didn’t expect to need one. Here it is wrapped in its own function:
Now we can use create_args()
to generate arguments and assign them to
formals(add)
.
So yeah…this was not exactly pretty, and again you probably don’t want to do it in 99% of scenarios, but it does seem to work. If anyone has ideas about how to make it cleaner, I’d love to hear!
* Without the expanding-to-destroy-all-everything-in-its-path part.