This post is about a practical application of a topic I discuss in my book. In my book, I prove mathematically that the ellipsis in a function signature is equivalent to passing a closure with the same arguments bound to the closure.

I’m currently modeling consumer spending behavior by modeling their credit and debit card transactions. I model each type of transaction (for example coffee shops or utilities) as a transaction stream. One such function produces a sequence of daily transactions as $f(\mu, \sigma, p) = \begin{cases}0, & U(0,1) < p \\ N(\mu,\sigma), & otherwise \end{cases}$.

x.daily(mu=0, sigma=1, p=0.1) %as%
{
function(t)
ifelse(runif(length(t)) < p, rnorm(length(t), mu,sigma), 0)
}


I had a situation where I needed to generate a transaction stream using a uniform random number instead. Since the above function exposes parameters specific to a normal distribution, it’s not possible to just use a function reference for the random number generator. Instead the parameters to the RNG must be passed along. This is a good use of the ellipsis argument since it will capture any unreferenced parameters.

x.daily(p=0.1, rng=rnorm, ...) %as%
{
function(t)
ifelse(runif(length(t)) < p, rng(length(t), ...), 0)
}


Now this can be called as f <- x.daily(.4, rng=rnorm, mean=11, sd=2); f(1:20), which will generate a sequence of events where each non-zero event is $N(11,2)$

On the other hand to generate a uniform distribution between [10, 20] is accomplished with f <- x.daily(.4, rng=runif, min=10, max=20); f(1:20).

What is the intuition around the use of the ellipsis? This is the core of the discussion and stems from how one would solve this problem if the ellipsis argument were not present. In this scenario the function would be defined

x.daily.1(p, rng) %::% numeric : Function : Function
x.daily.1(p=0.1, rng) %as%
{
function(t)
ifelse(runif(length(t)) < p, rng(length(t)), 0)
}


Calling the function is then done by passing a closure with the arguments predefined, such as x.daily.1(.4, function(x) runif(x, min=10, max=20)) or x.daily.1(.4, function(x) rnorm(x, mean=11, sd=2)). What’s remarkable is that both of these calls are equivalent to calling the original version that uses the ellipsis argument instead. Hence,

x.daily(.4, runif, min=10, max=20) ~ x.daily.1(.4, function(x) runif(x, min=10, max=20))

x.daily(.4, rnorm, mean=11, sd=2) ~ x.daily.1(.4, function(x) rnorm(x, mean=11, sd=2))

These relationships show that the ellipsis argument maps to the free variables of runif and rnorm. Clearly the function reference is arbitrary so this relationship maps to any specified function.

Armed with this equivalence relationship we can then symbolically transform a function from one representation to another. This becomes another tool used to reason about our programs and ultimately about the behavior of our models.