Another attempt at passing arrays as varargs (with implementation)

I think a generic parameter pack would probably be some new kind of structural type. You certainly can't apply postfix ... to those today, so I think the main question is, would we want to use pack... on a pack whose elements were known to conform to Comparable as "map each element of the pack to its PartialRangeFrom"? That seems a little more magical than is typical for Swift—wouldn't we want to somehow say map explicitly so the we could instead specify some other operation, like reduce?

(I suppose a generic parameter pack could instead be modeled as some sort of exotic tuple, and if it was, that would interfere with conforming tuples to Comparable. But given that we only recently won the war with 1-tuples, I'm not sure how that would work...)

It was mentioned earlier that 'splat' became a term of art because the asterisk resembles a splat could we use *** as an operator to avoid collision and maintain some amount of symmetry?

1 Like

I suppose in that case it might be possible to have effectively three overloads of ...:

  • The existing one with a comparable constraint
  • A new one constrained to Arrays
  • And yet another sometime in the future, with a layout constraint for parameter packs, assuming they're a new kind of structural type.

In that case, I think we could assume there'd be no operator overlap without overly restricting future design decisions. Maybe the latter two overloads could be functions in the standard library with custom compiler-defined behavior to avoid the need for lots of special casing in the type checker?

If the goal is for this feature's syntax to be in line with that of variadic generics, it still feels backwards to me to design the smaller, less complicated feature first. But, if we can do it without making too many harmful assumptions, then maybe it's still worth it.

Personally, I think the asterisk(s) invokes pointers of old which may be hard to depart from. I feel like the triple asterisk is pretty visually heavy.

2 Likes

How about postfix *..?

Although T... seems to have been ruled out now, I'd like to comment on it anyway. One drawback of writing the type explicitly is that you … have to write the type explicitly. There's a conceptual load in making sure you write the right thing.

For example, if you have this:

let a = [0, 1, 2]
… a as Int... …

you have to figure out that Int is the correct type. If you realize that you want this instead:

let a = [0, 1.1, 2]
… a as Double... …

then you annoyingly have two changes to make.

Personally, the alternative syntax I'd prefer, out of the things suggested in this thread, is:

 `a as ...`

where ... is just a sigil, not a postfix operator. If you think in these terms:

 `[0, 1, 2] as ...`

there's a nice internal anti-symmetry.

FWIW

4 Likes

Something like this (i.e. as ...) could also just be shorthand, similar to how you can write

let s = [1, 2, 3] as Set

and Set<Int> is inferred, but you can also write Set<Int> explicitly. This is all complicated by T... not being a real type, so you don't get a perfect analogy, but it roughly works conceptually.

1 Like

Hi all,

I'm working on an overhaul of this proposal to take variadic generics and other future features into account, but wanted to make an attempt to resolve some of the conversations around syntax first.

In my opinion, the best option at this point is to introduce an operator for the splat/spread operation. An operator will eliminate the need to restate types, and can work consistently for homogenous arrays and heterogenous parameter packs. In contrast, using as ... as a shorthand for the previously proposed syntax cannot be extended naturally to support tuple/parameter pack expansion, and is inconsistent with the existing behavior of the as operator as described here.

The existing ... operator has been brought up a lot as a possible candidate for this operation due to its symmetry with the syntax for declaring varargs. I've done a little bit of prototyping and it seems like adding array/tuple/parameter pack splatting as an alternate use case could be implemented in a source compatible way. However, if it were adopted as a universal spread operator, it would preclude future Comparable conformances for Array, and possibly tuples due to conflicts with the range operator. Perhaps more importantly, as noted here by @xwu , we would also be introducing a second, distinct meaning for an existing operator, which should probably be avoided if possible.

That brings me to my current suggestion, the prefix *. This operator has a few nice characteristics:

  • It has no existing meaning in Swift today, so there are no source compatibility concerns. There'd be no need to worry about weird interactions with Comparable conformance down the road.
  • There's precedent for using it as a splat operator in languages like Python & Ruby
  • It's been brought up in the past as a potential tuple splat operator. Using it for arrays and other splat operations as well would be consistent with that
  • ... could keep its existing, single meaning
8 Likes

The main downside of prefix * is that we've been thinking about using it in various ways to make pointer-manipulation code easier. If we did, we probably would need to make it special in much the way postfix ! is.

I'm not saying that those ideas are a firm plan or that they would necessarily rule out using it for splat too—just that it's a concern.

1 Like

The multiplication operator has no existing meaning?

1 Like

I'm talking about prefix *, not infix *. There's plenty of existing precedent for infix and pre/postfix operators with different meanings in Swift (& for example).

Good to know! I included a brief mention in my in-progress proposal revisions referencing the comments from when SE-29 was accepted, but I didn't realize there was still ongoing work looking into this. It's certainly something we'll need to keep in mind.

1 Like

Except that a postfix ... doesn't have a single meaning (syntactically). It already has two:

func foo(x: Int...) and let partialRange = 0...

One of which is already tied to variadics.

Would you mind sharing more as to why this would be the case? I think if I understood more fully I'd be more on board with letting postfix ... go. I still think, mainly due to symmetry, that ... is the best spelling by a wide margin.

Sure. The existing postfix ... in the standard library looks like this:

extension Comparable
  @_transparent
  public static postfix func ... (minimum: Self) -> PartialRangeFrom<Self> {
    return PartialRangeFrom(minimum)
  }
}

And a declaration of array splat ... would look something like this:

@_semantics("typechecker.varargExpansion") postfix func ...<T>(array: [T]) -> T {fatalError("Compiler Implemented")}

If Array ever gained a Comparable conformance, there'd be situations where you'd need awkward casts to select the intended overload (print and Any... arguments are what immediately come to mind).

I'm less concerned about Array becoming Comparable though, and more about tuples gaining the conformance, which is an oft-requested feature. If we used ... for Array, we'd probably want to use it for tuples as well, and they would run into the same overloading problem.

The existing use of ... to declare the type of a variadic argument isn't problematic today because it doesn't appear in an expression context. The spelling mismatch is unfortunate, but I think adapting ... creates too many problems down the road when working with tuple splatting and Variadic Generics, and confuses the meaning of the operator. I should have an updated proposal draft ready soon-ish which lays out the tradeoffs in more detail.

Lots of punctuation has multiple meanings, but we distinguish meaning through context. Here, the parameter declaration isn't using ... as an operator.

Thanks for the explanation — that was actually really helpful. I see, and now share, the concerns. All I have to say is...

... ditto.

With that context, I think the prefix * is the next best.

I've updated the proposal draft in the original post and gist to begin taking future directions into account, and reflect the switch to an operator. this isn't intended to be final, but I think it's important to start the discussion around consistency with tuple splat and Variadic Generics.

2 Likes

Sorry for the digression but I would love to know, out of curiosity, why wasn't this the original design. We're having such a big conversation about array and variadics that I wonder why wasn't treated as the same thing. Pardon my ignorance.

1 Like

I'm not a big fan of * as it doesn't transmit the meaning to me, but thanks for updating the proposal clarifying why ... is discarded. For people like me with some experience in other languages that use ... it would make it hard to not ask ?why not? on the proposal review :laughing:

FWIW, to the extent that the revised proposal's dismissal of as ... relates to what I suggested above, the reasoning doesn't actually apply.

I did not propose as ... as shorthand for as T.... I proposed the syntax as ... as syntax to indicate splatting. There is no need for consistency with as <sometype>, and there is no problem with heterogenous collections.

Taking a wider view, I'm also unhappy that the proposal treats "splat" as an operator, when it clearly isn't. Specifically, *<array> (or however it's spelled) as an expression has the same type and value as <array>.

What we're trying to do, surely, is to come up with syntax to treat an array as a list in parameter position. Trying to formalize this as an expression (via an operator) seems misguided to me, especially because that promotes artificial constraints on the design of the feature — and ends up complexifying both syntax and usability.

Of course, it's perfectly fine for the implementation of this feature to treat it as a pseudo-operator limited to specific contexts, if that's the most natural path to a solution. But let's not deform the feature because of that.

1 Like