Variadic Functions
Variadic functions are functions of indefinite arity, i.e., functions that accept a variable number of actual arguments. The most famous example of a widely used variadic function is the printf function of the C standard library.
General
Motivation
Variadic functions can be extremely convenient for operations that combine several values whose exact number and types are only known by the caller of that function. In particular, string formatting or the creation of containers such as sets can often benefit from variadic functions.
Security
Variadic functions, especially those provided by the C and C++ languages, contribute to security vulnerabilities (e.g., Venerable Variadic Vulnerabilities Vanquished or Variadic Functions How they contribute to security vulnerabilities and how to fix them). The main surface of attack are
- the lack of static type checking when processing the arguments, allowing arbitrary casts and memory accesses,
- the lack of static argument count checking, allowing access of arbitrary memory on the runtime stack , and
- the possibility of external control for argument processing, e.g., by
providing a string passed to a function of the
printf
family from external input as instr = read(); printf(str);
Basic approaches
There are basically three approaches to the implementation of variadic functions in current programming languages.
- Unsafe: The responsibility for proper type safety and respecting the argument count is left to the developer.
- Fixed type: Variadic functions are made type safe by permitting only a single type for variadic arguments, but the compiler ensures that the argument count at the call site matches the number of arguments that can be processed by the callee.
- Generic: The language ensures that both, the argument count and the actual types of all arguments are available and enforced to be respected.
No Variadic Support
Some languages do without variadic arguments and provide some alternative mechanisms that give the feel of variadic arguments.
Rust
Rust uses macros and recursive macro evaluation to emulate variadic calls.
Fortran
Fortran offers optional arguments that may be used to give the feel of a variable length argument list.
Unsafe Approaches
Languages that support unsafe variadic arguments include C, C++
C/C++
In C/C++, intrinsic functions permit processing variable argument lists. Processing requires a local data structure of typeva_list
that is initiated using va_start
, followed by an arbitrary number
of calls to va_arg
that extracts the next argument of a specified
type (that is neither checked statically nor at run-time) and finished by a call
to va_end
.
Fixed Type Approaches
C#/Java/Scala/Go
In these languages, variadic arguments are merely syntax sugar at the call site for wrapping a list of values of the same type into an array. The actual call is done using a single argument whose type is an array. Arrays in these languages properly check the correctness of indices on accesses to their elements, so argument count checking is implicit. All variadic argument have to be of the same type, which becomes the type of the array elements and is checked with the language's type checker.
GoLang uses its re-sizable arrays called slice for variadic function arguments.
In Fuzion, we can use the wrapping of arguments in an array as well to create
variadic functions, here is an example that concatenates
the String
representation of the arguments provided via
a Sequence
:
Generic Approaches
I am not aware of a programming language that permits compile time type checking and code specialization. Fuzion attempts to provide this.
Fuzion Approach
Basic Idea
Fuzion has open type parameters A type ...
that are used to
provide features like tuple
, choice
or Function
that depend on an arbitrary, but fixed list of types.
The idea is to use this to provide variadic functions. In fact, the
constructor of a tuple
is just that, it takes an arbitrary number
of type parameters and value arguments:
public tuple(public A type..., public values A... ) : property.orderable, property.hashable is
However, originally, the values
field of a tuple may only be
accessed for a concrete tuple type using a select suffix
like values.1
:
Operation on arbitrary types: typed_applicator
Instead, Fuzion defines a base library feature typed_applicator
as follows:
public typed_applicator(E type) is public apply(T type, e E, v T) B => abstract
This permits to perform an arbitrary operation on a value v
of an
arbitrary type T
.
Compile-time type checking
Since Fuzion permits compile-time type checks on type parameters
using infix :
as in if T : String then ...
, we can provide
different implementations variants within a redefinition of apply
and have the compile create only code for the actual variant that matches the
actual type parameter after monomorphization.
Folding values of arbitrary types: typed_fold
Calling an argument field whose type is an open type parameter returns an
instance of a compiler-generated feature that provides
a typed_fold
operation as follows:
public typed_foldf(E type, A type : typed_applicator E, e E, a A) E => e2 := a.apply T1 e v1 e3 := a.apply T2 e2 v2 .. e<n> := a.apply T<n> e<n-1> v<n> e<n>
Consequently, we can process all the elements as follows
The apply
function we defined in concat
will be
called with the type parameter and the value of each of the actual arguments
in the call.
We can now go one step further and check types against constraints to do more specific operations:
The actual code involves no reference type values, so no boxing is required
and all code could be inlined by the compiler. Furthermore, the code of `apply`
gets specialized for each actual type parameter, so the type constraints like
T : String
will be evaluated at compile time and the code of the
large if
will be be reduced to only one of the then
blocks or the final else
block.
We can provide helper features like container.typed_fold
that
avoid the need of declaring an inner feature like concat
by
inheriting as follows:
An alternative would be a helper container.variadic
that does not use a res
field, but instead a process
feature:
Comparison
Language
Example
|
Fuzion | Java | C/C++ | Python | JavaScript |
---|---|---|---|---|---|
sum of f64/i32 |
|
|
|
|
|
comparison |
✅ argument count checked |
✅ argument count checked |
❌ argument count not checked |
✅ argument count checked |
Conclusion
Variadic functions are poorly supported by existing languages, their use results in type safety issues that cannot be resolved at compile time.
In Fuzion, we provide two approaches:
- First, using `Sequence` as the argument type and syntax sugar for the creation of arrays. This provides the same level of functionality and type safety as Java/C#/etc.
- Alternatively, open type parameters provide full compile-time type checking and code specialization that avoids all runtime type checks and related runtime overheads for boxing and type checks.
What we might need is some more syntax sugar to reduce the boilerplate code. One approach would be to extends syntax sugar for `lambda` to support type parameters, see #5892.