Oftentimes in programming, there will be a function that can obviously be generalized to an arbitrary number of arguments, and it would be convenient to be able to handle each of the generalized versions of this function in some sort of uniform manner.

One common approach is to use template metaprogramming, which allows for source code creation at compile-time. In this post, we will demonstrate how one can leverage a sufficiently expressive dependently-typed type system to write such “variadic functions” at the term level, without using any metaprogramming. As an example, we will write a function that can curry a function of arbitrary arity (and whose tupled argument can be entirely heterogeneous). We wrote this currying function in Coq, although this approach should translate to most other dependently-typed languages out there. You can find the source code here.

We don’t claim that this approach is practically useful, but hopefully it can give some people a better idea of how powerful true dependent types can be.

Typing the function

Before we can even begin writing out the function, we need to be able to express its type. Using informal ellipsis-based notation, we would say that such a function should have the type

We see then that we’ll need some way to handle an arbitrary number of universal quantifiers, as well as arbitrarily-sized versions of the product and exponential on types.

As a first step, let’s define the type of n-ary functions on types:

Given such a type-level function, we can turn it into a type via the universal closure, which captures each type variable under a universal quantifier:

Once again, this can be defined recursively in a fairly straightforward fashion:

With that out of the way, we see that it suffices now to define the follow type-level function:

For the sake of brevity, we won’t go into exact detail on how to define this, but it’s worth mentioning some of the combinators on type-level functions that are required:

Given a type-level function, increase the arity by one by adding an unused argument at the end:

Given a unary type-level function and a type-level function, apply the unary function underneath all the lambda abstracted arguments:

Given a binary type-level function and two type-level functions of matching arity, apply the binary function to both underneath all the lambda abstracted arguments:

Defining the function

With the type defined, we can finally move on to actually defining the function in question. First, notice that for each arity, the currying function is the unique function with type

Thus, we will reduce the question of defining each currying function to the question of inhabiting each such type.

To show that each of these types is inhabited, we proceed by induction on the arity, starting at one (it does make sense to start at zero, but then the types for tuples would have ’s at the end, which is somewhat annoying).

For our base case then, we need something of type

Of course, we can use the identity function here.

For the inductive step, we assume we have something of type

and we need to define something of type

To see how this can be done, let’s see those two types with common subterms highlighted:

After removing the details, we have these two types:

which means we need something of type

and this is a basic STLC exercise.

After a bit of maneuvering to ensure that type arguments are passed around in the correct manner, we can finally define our variadic currying function. Let’s see how it works:

Compute variadic_curry 10.

yields

= fun (X X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 : Type) (x : uncurried_type 10 X X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10) (x0 : X) (x1 : X0) (x2 : X1) (x3 : X2) (x4 : X3) (x5 : X4) (x6 : X5) (x7 : X6) (x8 : X7) (x9 : X8) (x10 : X9) => x (x0, (x1, (x2, (x3, (x4, (x5, (x6, (x7, (x8, (x9, x10)))))))))) : curry_type 10

which is indeed what we wanted.