[swift-evolution] Variadic generics discussion

Brent Royal-Gordon brent at architechies.com
Tue May 31 20:29:45 CDT 2016


> There's definitely the possibility of going off the deep end with complexity like C++, but since we already have tuples as a primitive language feature, I think there's a straightforward language design that enables the most important use cases for variadics. If we have "tuple splatting" for argument forwarding, and some support for iterating tuples, like we briefly discussed in the context of (4 x Int) fixed-sized homogeneous tuples, that'd probably be enough.

I read the original proposal, started writing up a response along these lines, and then scrolled up to see what others had said. I'm glad you agree!

Here's my sketch.

* * *

In general, my preferred direction for this feature would tie variadics closely to tuples, and variadic types closely to tuple types. To wit:

1. We should dust off the old tuple indexing proposal (I actually forgot it was never reviewed) and get it through, along with tuple `count`, `map`, and probably a few other Collection-like operations which generate fixed signatures. (I'm going to use a method-style syntax, but a #function-style syntax would work too; we could even introduce a .#method-style syntax.)

	let tuple: (Int, Int, Int, Int) = (2, 4, 6, 8)
	print(tuple[tuple.0])			// => 6
	print(tuple.map { $0 / 2 }) 	// => (1, 2, 3, 4)

2. We should allow you to use analogous operators on the type of a tuple, too.

	let value: (Int, String)[1] = "Hello, world!"
	(Int, String).map { $0.Type }		// => (Int.Type, String.Type)

3. We should change our existing variadic parameters to create tuples, not Arrays.

	func print(_ items: (Any...), separator: String = "", terminator: String = "\n") {
		items.0	// Yes, that's a thing you can do

4. We should add a splat operator which unpacks tuples into (possibly variadic) parameters.

	print(tuple...)

5. We should allow you to define variadic type parameters.

	// Note: If you had written `items: Items` instead of `items: Items...`, it would've taken a tuple 
	// of various LocalizedStringConvertible types.
	func localizedPrint<Items: (LocalizedStringConvertible...)>(_ items: Items..., separator: String = "", terminator: String = "\n") {
		print(items.map { $0.localizedDescription }, separator: separator, terminator: terminator)
	}

6. We should extend splat to unpack tuple types into (possibly variadic) type parameters.

	typealias Pair = (String, Int)
	let table: Dictionary<Pair...> = [:]

(7. Optional pet-peeve fixer, which I will not actually demonstrate: Require unconstrained type parameters to be spelled `T: Any`, not just `T`. This will remove an asymmetry, in that a variable-length tuple of any type has to be spelled `T: (Any...)`, but a single parameter is just spelled `T`.)

What do we end up with here?

Zip:

	struct ZipSequence<Sequences: (Sequence...)>: Sequence {
		typealias Iterator: ZipIterator<Sequences.map { $0.Iterator }...>
		
		var sequences: Sequences
		
		func makeIterator() -> ZipIterator<Sequences...> {
			return ZipIterator.init(sequences.map { $0.makeIterator() }...)
		}
		
		init(_ sequences: Sequences...) {
			self.sequences = sequences
		}
	}
	
	struct ZipIterator<Iterators: (IteratorProtocol...)>: IteratorProtocol {
		typealias Element = Iterators.map { $0.Element }
		
		var iterators: Iterators
		
		init(_ iterators: Iterators...) {
			self.iterators = iterators
		}
		
		mutating func next() -> Element? {
			// Note: It may be too difficult to assign a type to this reduction;
			// something like the proposal's `#invert` may thus be necessary.
			// If it is added, I would hope that an analogous operation would 
			// be added to `Sequence`s of `Optional`s.
			let nextValues = iterators.map { $0.next() }.reduce(Optional( () )) { earlier, this in
				guard let earlier = earlier, this = this else {
					return nil
				}
				return earlier + (this)
			}
			guard let nextValues = nextValues else {
				return nil
			}
			return nextValues
		}
	}

Function application is basically just a use of the splat feature:

	// Note that the `args: Args` here is *not* `...`ed, so it takes a tuple.
	// Meanwhile, the `Args` in `(Args...) -> Result` does have `...`, so it's looking 
	// for arguments which that tuple could be splatted into.
	func apply<Args: (Any...), Return>(_ args: Args, to function: (Args...) -> Return) -> Return {
		return function(args...)
	}
	
	func method<Instance, Args: (Args...), Return>(_ name: String, on instance: Instance) -> ((Args...) -> Return)? {
		...
	}

MultiMap:

	func multiMap<Sequences: (Sequence...), Return>(_ sequences: (Sequences...), transform: (Sequences.map { $0.Iterator.Element }...) -> Return) -> [Return] {
		var returns: [Return] = []
		
		// You could do this by manually making iterators, but why bother when we already have the right abstraction?
		for elems in zip(sequences...) {
			returns.append(transform(elems...))
		}
		
		return returns
	}

Advantages of this approach, as I see them:

* Variadics are entirely a creature of parameter lists and type parameter lists; outside of those, they are always represented as a tuple. That means, for instance, that there is no need for variadics to reach into places like variables.

* `...` goes in exactly two places: After a type name within the definition of a tuple type, and after an expression being splatted into a parameter list. There's no proliferation of dots throughout the language or confusing leading-vs.-trailing dots thing.

* Many of these features are useful in non-variadic code. For instance, Collection-like tuples are a feature people want already.

* We stick closer to already-implemented features, rather than inventing a whole lot of completely novel stuff, like `#rest` and `#fold` and `#vector`.

Disadvantages:

* This leans rather heavily on tuple and tuple type features which will probably, at least for now, have to be built into the compiler. (You might be able to make them macros later.)

* This _may_ raise the grim specter of 1-tuples, but I'm not sure about that. (Perhaps even the concrete-tuple members should be hashed, like `#count` and `#map`? That would allow them to be applied to single values.)


-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list