Variadic Functions in R and Python

Siqi Zhang

2019/03/29

Categories: R Python Tags: Programming function variadic

In R, it is most usual to see all arguments defined explicitly for a funciton, such as in:

rnorm(n, mean = 0, sd = 1)
rnorm(2)
## [1] -0.8737635 -0.2613238

Other times, the function may take any number of arguments, such as in:

sum(..., na.rm = FALSE)
sum(1,2,3, c(4,5,6), 7:9)
## [1] 45

Functions like sum() are called variadic functions, where ... serves as the placeholder for the unspecified arguments. Note that the correct name of ..., according to the offical R Language Definition, is dot-dot-dot, not eclipse. It is not unusual to see smart people make this little mistake.

The ... can also take arguments The following code calls c() function, and uses argument names as the element names in the resulting vector.

c(foo = TRUE, bar = FALSE)
##   foo   bar 
##  TRUE FALSE

Writing a variadic function

You may decide on whether to use variadic arguments when writing your own function.

The following function takes an x and an y as arguments, and results the sum of x and y and 7. You must specify x and y, and only x and y, when calling this funciton.

add_seven <- function(x, y){
        x + y + 7
}

add_seven(x = 10, y = 10)
## [1] 27
add_seven(10, 10)
## [1] 27

This following function uses ... to take one, two or many numbers. There can only be one ... in one function call; and the ... stays the same through the whole call stack. That means you can pass along the ... to another function that also takes ....

add_seven_variadic <- function(...){
        sum(..., 7)
}

add_seven_variadic()
## [1] 7
add_seven_variadic(10)
## [1] 17
add_seven_variadic(10, 10)
## [1] 27
add_seven_variadic(10, 10, 10)
## [1] 37
add_seven_variadic(x = 10, y = 10, z = 10) # argument names ignored here
## [1] 37

You may also refer to the ... more than one time in the function body. The following function adds the ... with 7, and then itself again.

add_seven_variadic_twice <- function(...){
        add_seven_variadic(...) + sum(...)
}

add_seven_variadic_twice(10)
## [1] 27
add_seven_variadic_twice(10, 10)
## [1] 47

Capturing dot-dot-dot

It is also possible to capture the ... as a list. This is very helpful in advanced programming. The easiest way is just using list(...).

In the following example, tell_pocket() captures the ... as a list, and prints some messages accordingly.


tell_pocket <- function(...) {
        dots <- list(...)
        
        cat("I have: \n")
        for (i in seq_along(dots)){
                cat("  ", names(dots)[i], ": ", dots[[i]], 
                    "\n", sep = "")
        }
}

coins <- 2.25
bills = 5

tell_pocket(
        cellphone = "Nokia 3310",
        money = coins + bills
)
## I have: 
##   cellphone: Nokia 3310
##   money: 7.25

Dot Expansion

Using list(...) automatically evaluates any expresion in the ..., this is called dot expansion.

In our previous example, the money argument took the expression coins + bills, which is evaluated in the global environment, where tell_pocket() is called; the cellphone argument receives the string "Nokia 3310", which will always evaluate to itself.

However, we sometimes may wish to evaluate the arguments later, in a different context, or deparsing them into characters. In this case, we must avoid dot expansion and keeping them as-is. One way to do it is using match.call(expand.dots = FALSE)$...

In this example with tell_pocket2, we get values separately form the left_pocket and the right_pocket:

tell_pocket2 <- function(pocket, ...) {
        dots <- match.call(expand.dots = FALSE)$...
        
        cat("I have: \n")
        for (i in seq_along(dots)){
                dot <- eval(dots[[i]], envir = pocket)
                cat("  ", names(dots)[i], ": ", dot, 
                    "\n", sep = "")
        }
        cat("in the", deparse(substitute(pocket)), "\n\n")
}

left_pocket <- list(
        coins = 0.75,
        bills = 25
)

right_pocket <- list(
        coins = 2.25,
        bills = 10
)

tell_pocket2(left_pocket, money = coins + bills)
## I have: 
##   money: 25.75
## in the left_pocket
tell_pocket2(left_pocket, coins = coins, bills = bills)
## I have: 
##   coins: 0.75
##   bills: 25
## in the left_pocket
tell_pocket2(right_pocket, money = coins + bills)
## I have: 
##   money: 12.25
## in the right_pocket

The tell_pocket2() function used eval() to evaluate the expression in a specified environment, and used substitute() to capture the name supplied to an argument; these features fall into a grander scheme called Non-standard Evaluation(NSE) in R, which can be traced to R’s Lisp roots, and is far more sophisticated than that in Python. You may read more about NSE in this chapter from Advanced R by Wickham.

With Python

In Python, creating and using variadic functions is a little different. One uses *args and **kwargs to denote variadic arguments in place of ..., meaning arguments and keyword arguments. While it is legal to an arbitrary name succeding * or **, such as *foo and **bar, the previous example is the prevailing convention.

There is no need to capture the arguments, as *args and **kwargs are already list and dictionary, respectively.

Here are some examples in Python corresponding to previous R examples:

def add_seven(x, y):
    return sum([x, y, 7])

print(add_seven(10, 10))
## 27
def add_seven_variadic(*args):
    return sum([*args, 7]) 
    
print(add_seven_variadic())
## 7
print(add_seven_variadic(10))
## 17
print(add_seven_variadic(10, 10))
## 27
print(add_seven_variadic(10, 10, 10))
## 37
four_tens = [10, 10, 10, 10]
print(add_seven_variadic(*four_tens)) 
## 47

Note that the sum() function in Python is not a variadic function itself, and must take an iterable as its argument; in this case, a list.

def tell_pocket(**kwargs):
    print("I have:")
    for key, value in kwargs.items():
        print("  {}: {}".format(key, value))

coins = 0.5
bills = 5

tell_pocket(
    cellphone = "Motorola 300",
    money = coins + bills
)
## I have:
##   cellphone: Motorola 300
##   money: 5.5

Additionally, it is possible to use *args and **kwargs together in one function. But *args must occur before **kwargs.

def print_all(*args, **kwargs):
    for arg in args:
        print(arg)
    for kwarg in kwargs.items():
        print(kwarg)

print_all(1,2,3, x = 10, y = 20, z = 30)
## 1
## 2
## 3
## ('x', 10)
## ('y', 20)
## ('z', 30)

To learn more about variadic funcitons in Python, this article sums things up very well.