Variadic generics was referred to in "On the road to Swift 6". I think it may be a good idea to break down it into the following three steps compared to tackle variadic generics directly.
- Operations for tuples
- Variadic tuples
- Variadic generics
1. Operations for tuples
The following examples show two kinds of operations for tuples: map-like operations and reduce-like operations.
// map-like: (T) -> T
func squares<T1: Numeric, T2: Numeric>(of values: (T1, T2)) -> (T1, T2) {
values.map { $0 * $0 } // the type of `$0` differs for each element
}
squares(of: (3, 5.0)) // (9, 25.0)
// map-like: (T) -> T.AssocType
func firsts<C1: Collection, C2: Collection>(of collections: (C1, C2)) -> (C1.Element?, C2.Element?) {
collections.map { $0.first }
}
firsts(of: ([2, 3, 5], "XYZ")) // (.some(2), .some("X"))
// map-like: (T) -> U
func descriptions<T1: CustomStringConvertible, T2: CustomStringConvertible>(of values: (T1, T2)) -> (String, String) {
values.map { $0.description }
}
descriptions(of: (42, true)) // ("42", "true")
// reduce-like
func sum<T1: IntConvertible, T2: IntConvertible>(of values: (T1, T2)) -> Int {
values.reduce(0) { $0 + $1.asInt() }
}
sum(of: (3, 5.0)) // 8
Those operations can be statically unrolled. For example, the squares
function above can be converted as follows:
func squares<T1: Numeric, T2: Numeric>(of values: (T1, T2)) -> (T1, T2) {
({ $0 * $0 }(values.0), { $0 * $0 }(values.1))
}
Although map
and reduce
above are written using method-like syntax, it doesn't necessarily have to employ such syntax. What I want to discuss is semantics rather than syntax. Adding new syntax like below for the purpose is semantically identical.
func squares<T1: Numeric, T2: Numeric>(of values: (T1, T2)) -> (T1, T2) {
#map element in values { element * element }
}
Also operations as control flow is possible.
func sum<T1: IntConvertible, T2: IntConvertible>(of values: (T1, T2)) -> Int {
var sum: Int = 0
for value in values { // the type of `value` differs in each loop
sum += value.asInt()
}
return sum
}
It can be interpreted as follows:
func sum<T1: IntConvertible, T2: IntConvertible>(of values: (T1, T2)) -> Int {
var sum: Int = 0
do {
let value = values.0
sum += value.asInt()
}
do {
let value = values.1
sum += value.asInt()
}
return sum
}
Useful operations are not limited to map
and reduce
. For example, count
should be useful in some cases.
In those operations, e.g. in closures for map
and reduce
above, the type of the elements are constrained by the common constraints of all elements. So $0 * $0
is valid in the trailing closure of the map
in the square
function because $0
is constrained by Numeric
.
2. Variadic tuples
We can think about variadic tuples independently from variadic generics. In the following code, (Foo...)
works as a placeholder of tuples with any numbers of elements whose types are Foo
: ()
, (Foo)
, (Foo, Foo)
, (Foo, Foo, Foo)
and so on. While operations for tuples are just shorthand for normal tuples, those operations are necessary to operate on variadic tuples.
// map-like: (T) -> T
func squares(of values: (Int...)) -> (Int...) {
values.map { $0 * $0 }
}
squares(of: (3, 5)) // (9, 25)
squares(of: (3, 5, 7)) // (9, 25, 49)
// map-like: (T) -> T.AssocType
func firsts<T>(of arrays: ([T]...)) -> (T?...) {
arrays.map { $0.first }
}
firsts(of: ([2, 3], [5, 7]) // (.some(2), .some(5))
firsts(of: ([2, 3], [5, 7], [11, 13]) // (.some(2), .some(5), .some(11))
// map-like: (T) -> U
func descriptions(of values: (Int...)) -> (String...) {
values.map { $0.description }
}
descriptions(of: (2, 3)) // ("2", "3")
descriptions(of: (2, 3, 5)) // ("2", "3", "5")
// reduce-like
func sum(of values: (Int...)) -> Int {
values.reduce(0) { $0 + $1.asInt() }
}
sum(of: (3, 5)) // 8
sum(of: (3, 5, 7)) // 15
The syntax above have some problems in detail. For example, a way to specify relations between tuples is lacked. (Int...)
and (String...)
in the descriptions
function above must be a counterpart. But I do not go into detail of syntax here.
Because we gave up the arguments-are-a-single-tuple model, we need a way to flatten variadic tuples as arguments. Although it is desirable that variadic tuples and variadic arguments are united, I think it is hard without large-scale source-breaking changes.
// Ideal but collides with variadic arguments
func squares(of values: Int...) -> (Int...) { // Not `(Int...)` but `Int...`
// the type of `values` is `(Int...)`
values.map { $0 * $0 } // Not `Array.map` but a map-like operation for tuples
}
func sum(of values: Int...) -> Int {
// If the type of `values` is `(Int...)`, it is faster than `[Int]`
values.reduce(0) { $0 + $1.asInt() }
}
// When we want to handle variadic arguments as arrays
func foo(_ xs: Int...) {
// the type of `xs` is `(Int...)`
let ys: [Int] = [Int](xs)
...
}
Spreading tuple elements into a tuple can be also useful.
func tupleFrom<T>(head: T, tail: (T...)) -> (T, T...) {
(head, tail...)
}
let a: Int = 2
let b: (Int, Int) = (3, 5)
let c: (Int, Int, Int) = tupleFrom(head: a, tail: b) // (2, 3, 5)
3. Variadic generics
variadic generics can be realized by the combination of "1. Operations for tuples" and "2. Variadic tuples". The former realizes operations for elements with different types. The latter realizes handling tuples with the unfixed number of elements. Variadic generics has both characteristics.
Examples are here.
// map-like: (T) -> T
func squares<(T: Numeric)...>(of values: (T...)) -> (T...) {
values.map { $0 * $0 }
}
squares(of: (3, 5.0)) // (9, 25.0)
squares(of: (3, 5.0, i)) // (9, 25.0, -1.0)
// map-like: (T) -> T.AssocType
func firsts<(C: Collection)...>(of collections: (C...)) -> ((C.Element?)...) {
collections.map { $0.first }
}
firsts(of: ([2, 3, 5], "XYZ")) // (.some(2), .some("X"))
firsts(of: ([2, 3, 5], "XYZ", 0...42)) // (.some(2), .some("X"), .some(0))
// map-like: (T) -> U
func descriptions<(T: CustomStringConvertible)...>(of values: (T...)) -> (String...) {
values.map { $0.description }
}
descriptions(of: (42, true)) // ("42", "true")
descriptions(of: (42, true, "XYZ")) // ("42", "true", "XYZ")
// reduce-like
func sum<(T: IntConvertible)...>(of values: (T...)) -> Int {
values.reduce(0) { $0 + $1.asInt() }
}
sum(of: (3, 5.0)) // 8
sum(of: (3, 5.0, "7")) // 15
If those APIs using variadic generics are ABI-public or used from the same module, they can be specialized by unrolling in the same way as the operations for normal tuples.
square(of: (3, 5.0))
// then the specialized `square` function works like this
func squares(of values: (Int, Double)) -> (Int, Double) {
({ $0 * $0 }(values.0), { $0 * $0 }(values.1))
}
Without specializations, it can be handled at runtime and require some additions to ABI to represent something like generic closure objects. Although Swift does not support generic closure objects, it is a similar case to a runtime form of values whose types are specified by type parameters. Those values work similarly to values of generalized existentials, which are not supported.
It is remarkable that those variadic type parameters can be used only in the operations for tuples.
func foo<(T: AdditiveArithmetic)...>(_ xs: (T...)) {
let x: T = T.zero // â Compilation error
_ = xs.map { (x: T) in // â
OK
let y: T = x + x // â
OK
...
}
}
Summary
I proposed to break down variadic generics into three steps. Operations for tuples and variadic tuples can be discussed independently from variadic generics. I think they can be bases to discuss variadic generics. Because this is a rough sketch, it has a lot of issues, especially, syntactically. However, I think the semantics introduced here may be useful.