I know I've made this point in Language Workgroup meetings, but perhaps not fully in public before:
One of the first prototypes of the regex builder DSL used postfix operators like *
instead of named quantifiers like ZeroOrMore
, based on its use in the regex literal syntax. This resulted in code samples that looked something like this:
// Adapted from WWDC22 "What's new in Swift" -- not the shipping syntax!
let regex = Regex {
CharacterClass.horizontalWhitespace*
Capture(CharacterClass.noneOf("<#")+?)??
CharacterClass.horizontalWhitespace*
"<"
Capture(CharacterClass.noneOf(">#")+)
">"
CharacterClass.horizontalWhitespace*
ChoiceOf {
"#"
Anchor.endOfSubjectBeforeNewline
}
}
When we looked at code samples like this, we noticed that single punctuation characters in postfix position, like *
and +
, were easily lost in the clutter of identifiers and parentheses. A single incorrect or missing quantifier can dramatically change the behavior of a line in a builder, so rendering these behaviors near-invisible in the syntax seemed like a bad design, and we chose a different direction.
I think the same logic likely applies to using postfix *
for variadic generics. The invisibility of postfix *
is kind of a problem when all of these would be valid and would mean slightly different things:
printPack(tuple, pack*) // Concatenating tuple with pack
printPack(tuple, pack)* // Expanding tuple with pack
printPack(tuple.element*, pack*) // Concatenating tuple element pack with pack
printPack(tuple.element, pack)* // Expanding tuple element pack with pack
It would get worse in more complicated examples where the expansion was buried in a subexpression.
One thing I'll say for the map
-style approach in particular is that it makes the section of code covered by the expansion very clear:
printPack(tuple, pack.map { $0 }) // Concatenating tuple with pack
pack.map { printPack(tuple, $0) } // Expanding tuple with pack
printPack(tuple.element.map { $0 }, pack.map { $0 }) // Concatenating tuple element pack with pack
zip(tuple.element, pack).map { printPack($0.0, $0.1) } // Expanding tuple element pack with pack
Outside of expressions, though, I agree that it's not a great fit.
I don't have a single set of recommendations at this stage, but here are some things I'm thinking about:
struct VariadicZip<Collections: many Collection>: Collection {
var underlying: each Collections
typealias Index = (each Collections.Index)
typealias Element = (each Collections.Element)
subscript(i: Index) -> Element { (each underlying[i.element]) }
var startIndex: Index { ((each underlying).startIndex) }
var endIndex: Index { ((each underlying).endIndex) }
func formIndex(after index: inout Index) {
for (c, inout i) in each (underlying, index.element) {
c.formIndex(after: &i)
}
}
}
(Pardon any expression syntax mistakes—I'm still struggling a little with the scope of non-map
-style keywords, particularly in expressions like the one in the subscript
.)
Why?
-
many
andeach
get across similar ideas tovariadic
/pack
and...
/expand
without using jargon or overloaded symbols. I have to admit that themany
/any
rhyme is kind of pleasing too. -
many
is after the colon, not before the argument, because the fact that a generic argument is variadic feels type-y to me. (many
would be allowed as a standalone keyword in these positions, short formany Any
.) -
Why two different keywords? I like something like
each
in expression context where there's literal iteration happening, and it feels strange to have eithermany
oreach
used in opposing ways in a generic signature (labeling constraint) vs. a concrete type (labeling generic parameter).
An alternative that would avoid this last problem is:
struct VariadicZip<each Collections: Collection>: Collection {
var underlying: each Collections
// ...as before...
By moving the keyword before the generic parameter, it's no longer labeling the constraint in generic context, so it no longer has an opposing meaning in those two positions.
And, of course, I'm still thinking about map
-style syntax. This seems way clearer than version with an each
keyword, or ...
or *
suffix, or whatever else:
subscript(i: Index) -> Element {
(zip(underlying, i.element).map { $0.0[$0.1] })
}