This isn't going to make me popular but has to be said. I don't yet have an indepth knowledge of the Swift type system but it appears to me serious damage has been done changing functions so they no longer take a single argument. Although this is sustainable it is very hard to do so. In this article I'll briefly explain the problem and then show (partly) how to fix it.
Mathematical functions have one argument, and all type systems are based on mathematics. The core modelling structure used is the category, which is a collection of types and functions between them, in which, an associative binary partial operator called composition is provided. Composition is a key thing in programming. Generally several other features are provided: a cartesian product including its identity the unit tuple, and its dual sum type, sometimes called a variant, an enum in Swift, and its identity, the void type. Advanced languages also provide an exponential, written A -> B, which is a type representing function values. Note these are not the same as the functions between types, there's a difference between a function and its closure. In the type system, it is usual to also have a fixpoint (recursion) operator to simplify the typing and analysis of inductive types.
Programming is a constructive activity. Its not enough to have product types, we have to have a way to make them. This is typically done by providing a tuple constructor. If TYPE is the category we have been describing, then a pair is built with a bifunctor: pair: TYPE * TYPE -> TYPE, where TYPE * TYPE is the product of the TYPE category. Typically, languages provide a family of N-functors, for N a natural number. This allows functions to operate on multiple arguments in a two step process: first combine multiple values into a single value, so then the function can be applied. Note very carefully, the bifunction pair
is NOT a function. It does not live inside the category TYPE.
It is also possible to represent every function by a combination of an N-functor and the base function taking only one argument. This is what Swift has done. Unfortunately this removes the whole advantage of using a single pair of operations: tuple formation and function specification, for no benefit. In fact, the downsides are so large it makes the language unsustainable.
Consider the trivial problem that a function with two arguments cannot be applied to the result of applying another function. Composition is lost, and with it the underlying algebra is destroyed. For example you now have to write this:
let f: (A,B)->C = ...
let g: E -> A * B = ..
let e: E = ..
let (a,b) = g(e) // untypeable
let c : C = f (a,b)
I wrote the type g returns as A * B meaning a tuple. Swift used the wrong notation for tuple types. The LHS of the g application, and the argument of F do not have an actual value type. Its a disaster. The effects ripple throughout the type system. Type systems must be combinatorial. Nominal typing already escapes compositional behaviour deliberately, but the structural core must support composition.
Luckily there is a way to recover sanity. This is the very highest priority for the compiler team.
For every function with multiple arguments, there is a function taking a tuple instead: in the example:
func f_pack (x:(A,B)) -> C { return f(x.0,x.1); }
Conversely, for every function taking just a tuple, there is a function taking a list of the components instead:
fun f(a:A, b:B)->C { return f_pack ((a,b)); }
Generalising, there are two "generic" operator that can perform these operations. These operators are not needed for direct calls. As shown, the user can write them, even if its painful.
But for generics, they're absolutely essential. Stuff cannot be done at all without them. The alternative is to put families of generics in the library. Since such families are necessarily finite they can never cover all cases. If two functions are involved, a quadratic number of functions is required, if three its a cubic.
The obvious example in composition, which is linear:
operator compose<A,B,C> (f: A -> B, g:B -> C) {
func eta(a:A)->C { return f (g (a)); }
return eta;
}
Its fine, for one argument, but if f has two arguments, you have to write out another function which is a bit more verbose. If its three, its even worse. And this operation only involves one case of multiple arguments where the functions compose.
But with the two operators I specified, only the given generic is required, because we can just apply the pack operator to convert f to accept a tuple.
In higher order operations labels are an impediment, so they should be stripped by the pack operator as well.
The concrete syntax is a mess. Since (A,B) is a tuple, but (A,B)->C is not a mapping from a tuple but a function of two arguments, a function taking the corresponding tuple would have to be written ((A,B))->C. The notation (A,B) for a tuple was always wrong, it should have been A * B, the correct interpretation of (A,B) is that it is a pair of types: a product type is a single type.
The actual names of the operators I called pack and unpack don't matter, but it would be useful to have a single character for them, because they will have to be used extensively to recover sanity.
Adding these operators saves reverting the change to multiple arguments. I have no idea how that happened but it is probably not possible to undo the damage with a second major change, at least for some time. Providing the pack and unpack operations recovers the ability for generics to actually be general in a finite set of specifications instead of requiring an infinite family.
I will note that, if the non-associative N-ary constructor used for N tuples is augmented by a second type term, representing the same tuple type with a right associative head and tail form, then variadic generics are not required at all, since the head/tail form of a tuple is sufficient to recursively analyse any tuple.
Estimated time to implement: one day.
Impact on existing code: none, provided the names of the operators don't cause a clash.
Utility: Without these operators further evolution of the language is permanently stalled. No sane higher order operations can be provided. A type system in which the domain of a function doesn't have a type does not admit complete finite generic specifications.