How can varargs be implemented? We need some mechanism to signal the end of the argument list. This can either be
- a special terminator value, or
- the length of the vararg list passed as an extra parameter.
Both of these mechanisms can be used in the context of currying to implement varargs, but proper typing becomes a major issue. Let's assume that we are dealing with a function sum: ...int -> int
, except that this function uses currying (so we actually have a type more like sum: int -> ... -> int -> int
, except that we don't know the number of arguments).
Case: terminator value: Let end
be the special terminator, and T
be the type of sum
. We now know that applied to end
the function returns: sum: end -> int
, and that applied to an int we get another sum-like function: sum: int -> T
. Therefore T
is the union of these types: T = (end -> int) | (int -> T)
. By substituting T
, we get various possible types such as end -> int
, int -> end -> int
, int -> int -> end -> int
, etc. However, most type systems do not accommodate such types.
Case: explicit length: The first argument to a vararg function is the number of varargs. So sum 0 : int
, sum 1 : int -> int
, sum 3 : int -> int -> int -> int
etc. This is supported in some type systems and is an example of dependent typing. Actually, the number of arguments would be a type parameter and not a regular parameter – it would not make sense for the arity of the function to depend on a runtime value, s = ((sum (floor (rand 3))) 1) 2
is obviously ill-typed: this evaluates to either s = ((sum 0) 1) 2 = (0 1) 2
, s = ((sum 1) 1) 2 = 1 2
, or s = ((sum 2) 1) 2 = 3
.
In practice, none of these techniques should be used since they are error-prone, and don't have a (meaningful) type in common type systems. Instead, just pass a list of values as one paramter: sum: [int] -> int
.
Yes, it is possible for an object to appear as both a function and a value, e.g. in a type system with coercions. Let sum
be a SumObj
, which has two coercions:
coerce: SumObj -> int -> SumObj
allows sum
to be used as a function, and
coerce: SumObj -> int
allows us to extract the result.
Technically, this is a variation of the terminator value case above, with T = SumObj
, and coerce
being a un-wrapper for the type. In many object-oriented languages, this is trivially implementable with operator overloading, e.g. C++:
#include <iostream>
using namespace std;
class sum {
int value;
public:
explicit sum() : sum(0) {}
explicit sum(int x) : value(x) {}
sum operator()(int x) const { return sum(value + x); } // function call overload
operator int() const { return value; } // integer cast overload
};
int main() {
int zero = sum();
cout << "zero sum as int: " << zero << '\n';
int someSum = sum(1)(2)(4);
cout << "some sum as int: " << someSum << '\n';
}
sum
is0
without an argument and recursively calls itself with an argument.reduce
?args
:empty
,head
, andtail
. Those are all list functions, suggesting that maybe the easier and more straightforward thing to do would be to use a list where the variadic stuff would be. (So,sum [1, 2, 3]
instead ofsum 1 2 3
)