[Discussion] Eliding `some` in Swift 6

I don't have any comments about the timing, but I disagree with this direction in general. I think that, given a Foo protocol, having a clear way to distinguish the 3 forms Foo, any Foo and some Foo is really useful, because in actual code they mean 3 different things, and the distinction between writing Foo as an existential and Foo as a generic constraint is a the root of the introduction of any. In Swift 5.7, I now have a plan to progressively explain how to use protocols in generic contexts, which goes from the simplest case to the most complex:

  • some Foo: some concrete type that conforms to Foo, usually the best option;
  • any Foo: any type that conforms to Foo, which gives me a chance to explain what an existential is, and in what cases it makes sense (for example, heterogenous collections);
  • just Foo: a constraint for some type to conform to Foo, used when declaring explicit generic annotations (more advanced).

I think that conflating again 2 of those would be a mistake.

26 Likes

I think with the introduction of explicit any and the mandatory use of some Swift Generics has never been more easy to explain and reason about. Going back to encourage the use of bare protocol name as a type (and changing the meaning of it) seems like going backwards and I'm honestly kind of surprised that this is even being discussed.

16 Likes

Yes. Generics are the norm and existentializing is less common and less desirable, so it makes sense for the developer to say any T when they explicitly want to take the existential. At least in some imaginable timeline.

I think there's a "how to do the change" angle to this as well. When the public keyword becomes involved, new concerns emerge and reasoning can reverse.

Changing internal code from being unintentionally-existential to generic-and-still-works seems like a desirable change. Impact can be managed with all the normal migration tools and techniques. However, if that existential type appeared in a public parameter, then the change is both source and ABI breaking, and that impact is harder to manage and track.

12 Likes

I think this discussion in general should revolve around the first case of reinterpreting the bare-protocol syntax to ease the migration process. If easing the migration this way is not desirable, I think it’s better to wait until Swift 7 is right around the corner before having this discussion.

If we have this discussion prematurely and not about easing the migration, we miss the important context of how real-world users perceive generics and existentials. How users intuit the some syntax is not at all a given this early on in the migration. We might see that users coming from other languages have a hard time distinguishing between any and some, or even stick to any since that’s the equivalent in their former language (though that’s highly unlikely). How often any is used after the migration in idiomatic Swift will also affect the desirability of this feature. If users turn out to use existentials early in their Swift programming journey, then this model of progressive disclosure will do more harm than good. The bare-protocol model would hide the underlying abstraction mechanism (existentials vs generics), but users would soon have to learn about these differences, and hence the actual meaning of the bare-protocol syntactic sugar. Of course, we can debate the desired design principles for this feature and reach a satisfactory conclusion, but right now this will be very theoretical and less time-effective than if we wait.

2 Likes

Isn't the last bullet point a non generic case? Example:

protocol Item { ... } // no Self or associated type here
var items: [Item] = []

It may make sense to level the field and do the opposite of what this discussion topic suggests: introduce the third keyword for the third case:

some Foo
any Foo
protocol Foo // a strawman syntax for what was previously "just" Foo.

I'd like to chime in and quickly say, I'm fully supportive of inferring some in the just Protocol case in Swift 6 with the proviso it might be more useful to infer any when just Protocol is used in a collection if that doesn't sound too arbitrary a rule. This would seem to be the best way to mitigate a good deal of the source breakfulness of the introduction of the distinction between any and some while achieving most of the stated objectives of the original proposal and I'm glad we're having this conversation.

I'm very much of the opinion source breaking things then introducing the "elision" later out of fear they somehow don't have the same deep meaning would be the worst of all possible migration paths. In terms of ABI stability, I assume the mangled name and .swiftmodule would encode the distinction the compiler has inferred when the code was compiled and this shouldn't present a problem.

I'll take any elision we can get, as some and any are both noise in most contexts—especially to the target audience of all the recent syntactical generics improvements. It seems to me that the elision…

  1. should mean preferentially some, as detailed here…
  2. but also mean any, if necessary for compilation…
  3. and thereby undo the new requirement of any

If you can manually override some to any, where would this cause a problem? Just slowed compilation if you aren't explicit?

I have already addressed the idea to default to some sometimes and any other times, and why I am strongly opposed to the idea, in the Alternatives Considered section

6 Likes

I agree - I'm starting this discussion now because I want feedback on programmers' experience internalizing and applying the new generics features in Swift 5.7. In particular, I'm interested to know things such as:

  • Whether the some keyword in opaque result types is more clarifying now that (hopefully) programmers are starting to gain a better understanding of opaque result types. Pre-5.7, I'd received a lot of feedback that people don't know what some really means and they just stick it into their SwiftUI views to satisfy the compiler.
  • What kinds of code programmers are able to modernize in Swift 5.7.
  • Where programmers are able to change any to some (or insert some in front of a bare protocol name in existing code), which will inform how significant eliding some would be for minimizing code churn in the Swift 6 transition.

Even if we don't elide the some keyword ever, this information would be extremely informative for the Swift 6 migrator and for general refactoring actions, as I've been considering possibly having the migrator prompt programmers to consider changing existing uses of any to some where possible.

I think this sort of feedback is useful! I share your perspective on a "two-step" migration and it's appealing to me to make the Swift 6 transition as mechanical as possible, but I know other programmers are more concerned about the sheer amount of code changes that will be necessary to adopt Swift 6. I think there's an opportunity here to enable fewer changes, so I'm interested in evaluating how big of an impact eliding some would have. I agree that easing the migration is a pretty compelling reason to explore this direction, because I'm also sympathetic to @Paul_Cantrell's point above:

I encourage folks here to spend some time checking out some of the resources on generic programming in Swift 5.7, try it out in your own projects, and share your experience! I anticipate gathering feedback over many weeks or months before deciding whether to pitch and implement the idea.

9 Likes

This brings up another point of consideration: Users are starting to settle in with the concept of opaque result types, which have always been spelled with some since their introduction. The eventual elision of that some would certainly make things even more ergonomic, but enabling that now would be a source of churn for an already shipping feature that users are just starting to get used to (and any sort of piecemeal introduction of some elision would end up with a "smear" of inconsistent transitional states in the language rules across multiple releases).

While Swift 6 would be the first version with mandatory any for existentials, it is not the case that it'll be the first introduction of some. As these features are not being introduced synchronously, choosing this moment in Swift's evolution for some elision isn't all upside in terms of being uniquely elegant for ease of transition.

I think it bears remembering that for those of us on these forums, the non-sugared final state we're headed towards with explicit some and any has been under discussion for years; personally, I'm so ready to see us at least get to a place where some elision can be considered (whether or not it's ultimately adopted) as the final capstone in this process. But for users who don't hang out here, opaque result types are very new, and explicit any for protocol existentials hasn't even been released.

9 Likes

Explicit any was released with Swift 5.6. Now, that was only a few months ago, it isn't mandatory so I'm sure it's not widely adopted yet, and I certainly acknowledge that most programmers weren't aware of it or what it meant until this week with the comprehensive overviews of generics in Swift 5.7, so I do understand your point.

5 Likes

I’m almost inclined to want to go in the opposite direction altogether on this, and require explicit some for extensions too instead:

extension some Collection { … }

Especially for when existentials become extensible:

extension any Collection: Collection { … }

extension some Collection<String> { … }

It would help reinforce the mental model for generics vs existentials in all situations.

Granted, code churn would be immense on this, though the pattern seems simple enough to detect and offer auto-migration for.

3 Likes

That wasn't clear to me because of "heuristics" and "most useful":

You've probably already addressed above how what I'm thinking is incomplete and shortsighted, but I can't tell without code examples. I haven't thought of cases where there can be ambiguity in whether to default to some or any, given the way I'm currently imagining what you're proposing:

"Implicit some, overridable by explicit any, would be the same concept as other explicit and implicit spellings." E.g.

1
1 as any BinaryFloatingPoint

1... as Sequence<Int> // Doesn't compile currently—good candidate for becoming implicit `some` Sequence<Int>.
1... as any Sequence<Int>

I would think that always being explicit in protocol definitions, and when satisfying existential return types, would be useful, but considering some can't be used for either, I don't think there's any reason for any to be required either.

For example, this compiles, but won't, without any in both places, in Swift 6 as planned, I think:

protocol P {
  var p: P { get }
}

struct S: P {
  var p: P { self } // No, `some` is not an option here either!
}

And this currently requires any, even though some is not an option.

protocol P {
  associatedtype T
  var p: P { get }  // Use of protocol 'P' as a type must be written 'any P'
}

I don't have a strong opinion on this idea, but I do personally prefer the explicitness of both keywords. In fact I would even welcome their addition on types that supported sub-typing (e.g. any UIView being any subtype of UIView or itself, some UIView being a concrete subtype of UIView, UIView without a keyword be just UIView and not a subtype).

That aside, wouldn't the merge of the protocol P as a constraint with some P eliminate the conformance constraint or create a special case at least?

If bare P were some P then:

func foo<T: P>(…) // becomes illegal
// and needs to be rewritten as
func foo<T == P>(…) // because P is some P

Please correct me if I'm wrong.

2 Likes

Hear, hear! Generics is in a really good place right now; we have a really rich set of capabilities, which allow us to express a lot of abstractions. There are still gaps, but it feels like we can identify and explain them a lot more easily now, because we have this new language model based around preserving and erasing type identity with some and any.

I'm not sure about extending existentials, though. Whether or not it can technically be supported, it usually isn't what you want. I don't really consider existentials like the any Collection box to be an entity with equal importance to a written type - it's just a box, and it is defined by its contents.

If I was comparing a program to garden, types such as Collection (or custom protocols) are like the flowers, and existentials (such as any Collection) are like flower pots - they exist as physical objects, certainly, but they are not the main actors in this picture. They are supporting infrastructure that I would prefer was as transparent as possible.

Once you start thinking of the box as a separate entity, you start getting in to "Foo does not conform to Foo" land, and we know from that experience that Swift developers don't think of existentials that way. Like, imagine if an existential could independently conform to Collection or Equatable, regardless of the semantics of what's inside the box? It's an interesting thought experiment and perhaps can be implemented, but it's just not an intuitive language model.

In terms of expressivity, there is only one thing that an extension on any Collection can do that an extension on Collection cannot (I think?) - and that is that you can have a mutating method which changes the underlying storage type:

extension any Collection<some Hashable> {

  mutating func changePlaces() {
    if let array = self as? Array<Element> {
      self = Set(array)
    } else {
      self = Array(self)
    }
  }
}

I'm not sure this is so valuable that it's worth adding extensions to existentials to support it, though. You could still write this as a regular method, taking an inout any Collection<some Hashable> parameter.

1 Like

Personally I think it's important, during the time users will need to get used to some vs any, to have to think about it themselves so that the distinction can sink in.

I can see arguments for inferring the right choice in most contexts, like var x: [any P] etc and make it so that people will have less of their code to change during the migration all while we'd still magically nudge them in the right direction (eg inferring some where it makes more sense). But I'm concerned that while this view of the suggested approach here totally has its merits, it will miss the opportunity to have users being forced to think about it and thus better understand the difference and have the meaning of some vs any sink in.

What if instead we don't allow eliding some at least just yet… but make the transition easier (while still explicit to teach users) by focusing on best in class fix-its? That is, have the compiler infer via some heuristics what it think would be the right choice (some or any) to migrate a bare protocol based on the context and why, and provide a fix-it message that explains the rationale of why it'd recommend one rather than the other. That way users (especially the ones who might not necessarily have followed SE discussions) can learn and understand better in which context it makes more sense to use which and why, but they could also at some point decide to just apply the first fix-it (= the one most recommended by the compiler based on these heuristics) kind of blindly once they think they understood it, to migrate faster.

Or on a similar idea, maybe the solution is to provide a migration assistant, which would offer checkboxes for each common cases we identified above so that users can auto-migrate eg all of var x: [P] to var x: [any P] and other cases as well, but choose to not auto-migrate (and instead go thru each manually) for other contexts to take their time to analyze those deeper case by case.


I know, all that does not answer the "should we elide some at some point in the future" and more addresses the timing question, but at least I think it provides a different take to the "if we do that note that will ease the migration" idea that some people shared above.

2 Likes

If you can implement best in class fix-its, you can implement eliding and you can't fix-it an upstream dependency if the Package didn't specify the Swift version.

I'm just not convinced the average developer who happens to use a protocol or two is very interested in having to have the meaning of some vs. any sink in. Imposing this seems like an "eat your greens" form of sadism given the widespread source breaking, dependency management nightmares and churn for library maintainers that forcing the transition onto the user would result in.

At this point I'd be evaluating implementations of eliding using the compatibility suite and then the Swift Package Index tuning it to quantify if it were possible to minimise source breaking rather than making decisions we shouldn't elide at all on principle. Maybe it wouldn't be guaranteed to work out in all cases or possibly not at all but I feel we should at least try.

2 Likes

I have very hard time understanding why code migration has somehow become more important than good language design. After YEARS of waiting the ”any” to be finally added to the language and the changes inow being ntroduced in phased way, the code migration is really an issue once for one major release. It’s basically irrelevant in the long run (5-10 years in the future).

The focus really should be in designing the best possible language for the future instead of forcing language design to compromise due to short term inconveniences.

As I said before, the recent any/some proposals that have been accepted highlight a very bright future for swift to strike a good balance between powerful features, while still being relatively easy to understand and easy to learn language with progressive discosure. I can easily imagine doscussions about elision and magic behaviors in perl forums, but really disappointed to see such suggestions here.

8 Likes

Sure it is:

protocol P { }
struct S: P {
    var p: some P { self }
}

Your example doesn't compile because you have a requirement of type any P, which cannot be fulfilled by an implementation of type some P.

I know. I'm saying that because you can only satisfy any requirements with any, requiring a keyword is the wrong choice.

That's different, and I'm in favor of making the some keyword implicit there, as proposed.