Thanks for getting the discussion started with such a thorough initial writeup.
My overall concern is this:
- on the one hand, I *very much* understand—and support—the idea of starting
simple and adding functionality later
- on the other hand, this is *already* a rather complicated proposal
syntactically, semantically, and probably also in implementation
- on the gripping hand, I’m concerned that despite being so complicated, it’s
perhaps too limited to pay for itself as-proposed
Or, put differently, if you believe there’s an optimal point on the
complexity/functionality for a v1, I worry a bit after reading it that it’s
buying too little practical functionality for the complexity it’ll bring, and
that something a *little* more complex might provide a *lot* more practical
utility.
To that end, I’ve provided some concrete-ish examples of things it’d be nice to
be able to do via variadics and the additional features they would either
*need* or at least *benefit* from having available.
All of these are very much “nice to have” features that need not be included in
a first version, but they each illustrate one possible complexity/capability
trade-off.
A.1: Support for uniform variadic-parameter associated-type constraints?
Consider trying to write a variadic form of this sequence:
// given sequences a, b, provides a sequence equivalent-to
// zip(a,b).lazy.map() {
// ( min($0.0,$0.1), max($0.0,$0.1) )
// }
struct ExtremaSequence<A:Sequence,B.Sequence where A.Iterator.Element ==
B.Iterator.Element, A.Iterator.Element: Comparable> {
private let a: A; private let b: B
}
struct ExtremaIterator<A:Iterator,B.Iterator where A.Element == B.Element,
A.Element:Comparable> {
private var a: A?
private var b: B?
mutating func next() -> (A.Element,A.Element)? {
guard let nextA = a?.next() else {
a = nil; b = nil;
return nil
}
guard let nextB = b?.next() else {
a = nil; b = nil;
return nil
}
if nextA < nextB {
return (nextA,nextB)
} else {
return (nextB,nextA)
}
}
}
…would it be necessary to write the type’s generic parameters like this:
struct VariadicExtremaSequence<E:Comparable,S… where S:Sequence,
S.Iterator.Element == E>
…or would there be some way of writing a constraint like “all `S.Iterator` must
have same `.Element`?”?
A.2: Support for non-uniform "parameter-to-parameter” constraints
As an example:
protocol ValueTransformer {
associatedtype Input
associatedtype Output
func transformedValue(for input: Input) throws -> Output
}
// how to express this (if possible), mock syntax below
struct LoggingValueTransformerApplier<
T:ValueTransformer
where
…T[i],T[j]… => T[i].Output == T[j].Input> : ValueTransformer {
typealias Input = #first(T…).Input
typealias Output = #last(T…).Output
private let (transformers) = (T...)
func transformedValue(for input: Input) throws -> Output {
// evaluate incrementally, logging input => (output | error) step-by-step
}
}
…something like the above is definitely a "nice to have", but not having it
will close off certain use cases.
B: Integer-Based Indexing
I see it’s been brought up already, but I’d *strongly* suggest making support
for integer-based “indexing” into variadic packs a priority.
What concerns me is that without support for that, I suspect a lot of code
would have to get "written twice” if using the “recursive” API on parameter
packs.
Here’s a mock example:
// a “weaker” dictionary-like protocol
protocol LookupTable {
associatedtype Key: Hashable
associatedtype Value
subscript(key: Key) -> Value? { get }
}
/// Does a cascading search for values through a possibly *heterogeneous*
/// collection of backing lookup tables…caching results to avoid subsequent
searches
struct HeterogeneousCascadingLookupTable<K:Hashable,V,T…
where
T:LookupTable,
T.Key == K,
T.Value == V> : LookupTable {
private let (tables): (T…)
private var valueCache: [K:V] = [:]
private var missingKeys: Set<K> = []
// implementation *with* integer-based indexing:
subscript(key: K) -> V? {
get {
guard !missingKeys.contains(key) else { return nil }
if let cached = valueCache[key] { return cached }
for index in 0..<#count(T…) {
if let v = tables[index][key] {
valueCache[key] = v
return v
}
}
missingKeys.insert(key)
return nil
}
}
// implementation without integer-based indexing (or equivalent mechanism):
subscript(key: K) -> V? {
get {
guard !missingKeys.contains(key) else { return nil }
if let cached = valueCache[key] { return cached }
if let value = self.lookupValue(for: key, in: self.tables) {
valueCache[key] = value
return value
} else {
missingKeys.insert(key)
return nil
}
}
}
// recursive lookup implementation (necessary b/c our type itself
// isn’t defined by recursively-nesting itself)
private final func lookupValue<U…
where
U…: LookupTable,
U.Key == K,
U.Value == V>(for key: K, in tables: U…) -> V? {
return #first(tables)[key] ?? self.lookupValue(for: key, in: #rest(tables))
}
}
…which isn’t the end of the world, but having to write a separate recursive
implementation of each method that needs to step-through the variadic
parameters would spread things out a bit, it’d seem.
(Yes, this specific thing could be written today w/out variadics, but it
illustrates the tradeoff here).
C: “Variadic-Sum” / Structural-Union?
I mentioned this before in a response to the manifesto email, but will
reiterate it here: there’s a fair amount of useful things you could do with
variadic generics *if* there was also some similar way to get a “structural
union”.
A simple motivating example is something like a `Chain` sequence (analogous to
`Zip`); e.g. something for which `chain(a,b)` is the sequence of ‘the elements
in `a`, then the elements in `b`.
Although the proposed variadics make the sequence itself easy to write:
// for now I'd assume we need the `E` as a separate type parameter here,
// but recall the previous point
struct ChainSequence<E,S… where S:Sequence, S.Iterator.Element == E> {
private let (…sequences): (S…)
init(…arguments: S…) { … }
}
…the iterator is a bit more complicated. Without “structural unions” you’d
probably wind up with something like this for a variadic implementation:
struct ChainSequenceIterator<E,I… where I:Iterator, I.Element == E> {
private var (…iterators): (I?…) // <- I hope this works
mutating func next() -> E? {
// find first non-nil iterator, try to get next element from it;
// if next is non-nil, return that, otherwise nil it out and
// find the *next* non-nil iterator, etc….
}
}
…which could be made to work, but is wasteful in some ways (you have space to
store n iterators…). You can make it a little cleaner if you have some way of
doing integer-based indexing into the variadic parameters, but that doesn’t
change the space requirements (it’s still going to need room for all `n`
iterators + bookkeeping).
If, however, you have some variadic-compatible structural unions, you can write
it as (roughly):
struct ChainSequenceIterator<E,S… where S:Sequence, S.Iterator.Element == E> {
private let source: ChainSequence<E,S…>
private var iterator: (S.Iterator |…) // (meant to be ~ `(S1.Iterator |
S2.Iterator | S3.Iterator | …. | SN.Iterator)` )
private var done: Bool = false
mutating func next() -> E? {
// if not `done`, try `next` on current iterator in `iterator`
// if non-nil, return, otherwise advance to next iterator, etc., ...
}
}
…(how much space this actually saves depends on the size of `source` vs the
size of the iterators, but it’s *possible* to save space this way).
Variadic generics *can* be added w/out structural unions — they’re a pure
"nice-to-have" — but having support for variadic "product types" w/out
corresponding variadic "sum types" is going to complicate-or-prevent certain
constructs.
D: Make Parameter-Packs Named
I understand the appeal of the `…T`-style syntax, but I’d at least consider
giving parameter-packs a more-concrete existence, e.g. something like this:
// strawman declaration syntax:
struct Foo<I,J,K#ParameterPack> {
// `K` here will refer to the variadic parameter-pack itself;
// cf the `typealias` below , but basically K ~ the tuple of its types
typealias KValueTuple = #tuple(K) // == tuple of values of type K.0, K.1,
etc.)
typealias KTypeTyple - #typeTuple(K) // == tuple of types like K.0, K.1
typealias FirstK = #first(K)
typealias LastK = #last(K)
static var kArity: Int { return #count(K) }
// and so on
}
// straw man point-of-use syntax, can be adjusted of course:
let concreteFoo = Foo<I,J,K:(K0,K1,K2,…,Kn)>
…which complicates the grammar, but IMHO feels a lot nicer than having a lot of
"implicit rules" about what `…` means and so on.
> On May 28, 2016, at 3:03 PM, Austin Zheng via swift-evolution
> <[email protected]> wrote:
>
> Hello swift-evolution,
>
> I put together a draft proposal for the variadic generics feature described
> in "Completing Generics" as a major objective for Swift 3.x. It can be found
> here:
>
> https://github.com/austinzheng/swift-evolution/blob/az-variadic-generics/proposals/XXXX-variadic-generics.md
>
> <https://github.com/austinzheng/swift-evolution/blob/az-variadic-generics/proposals/XXXX-variadic-generics.md>
>
> It adopts the syntax and semantics that are described in Completing Generics,
> and attempts to remain as simple as possible while still being useful for
> certain use cases (although I think there is still room to simplify). The
> proposal contains sample implementations for four use cases:
>
> - Arbitrary-arity 'zip' sequence
> - Arbitrary-arity tuple comparison for equality
> - Tuple splat/function application
> - Multiple-arity Clojure-style 'map' function
>
> There is a lot of scope for design refinements, and even for alternative
> designs. With enhanced existentials, there was already an informal consensus
> that the feature would involve composing some protocols and class
> requirements, and placing constraints on the associated types, and most
> everything else was working out the various implications of doing so. That's
> not true for this feature.
>
> In particular, I'm interested to see if there are similarly expressive
> designs that use exclusively tuple-based patterns and no parameter packs. I
> think Rust once considered a similar approach, although their proposal ended
> up introducing a parameter-pack like construct for use with fn application:
> https://github.com/rust-lang/rfcs/issues/376
> <https://github.com/rust-lang/rfcs/issues/376>
>
> Feedback would be greatly appreciated. Again, be unsparing.
>
> Best,
> Austin
>
> _______________________________________________
> swift-evolution mailing list
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution