[Discussion] Eliding `some` in Swift 6

Well, conformance and placeholders are dealing with the same situation, a type that conforms to a protocol. But they are used for saying different things about that situation. I mean we care enough about the difference to have a specific notation for "X conforms to P", namely the ":". We could just say `X == some P".

1 Like

Is there a different term we could use that doesn’t conflict with

The term for some P is an opaque type of P. Is that what you mean?

Yes, my analysis is correct. A protocol extension on Collection has a generic signature <Self> where Self: Collection, along with any other requirements in the extension's where clause. If we had parameterized extensions, a protocol extension on Collection is effectively sugar for:

extension <Self> Self where Self: Collection { ... }

This is the beauty of methods in protocol extensions! It's an extremely natural way to write generic code without needing to understand all the intricacies of generic signatures.

The reason why you can call protocol extension methods on any types is because of implicit opening of the base type (which was generalized to normal function arguments in Swift 5.7). For example, a method call on any Collection will open the base type and call the method directly on the underlying type. This is also why it's not possible to call static methods in protocol extensions on any types - you instead need an existential metatype with an underlying concrete metatype that can be opened to call the method directly on the concrete metatype:

protocol P {}

extension P {
  static func test() {}
}

func callTest(metatype: any P.Type) {
  (any P).test() // error: Static member 'test' cannot be used on protocol metatype '(any P).Type'

  metatype.test() // okay
}
5 Likes

Thank you! It now makes sense that extension P only extends some P, but any P behaves as if extended by the implicit opening of the base type.
I think we should rephrase it as extension some P if we won't introduce eliding some in Swift 6.

2 Likes

While not its intention, this code makes one of the clearest cases for why some should be elided. It's true that essentially extending a protocol is extension some Protocol, to require an explicit some to all protocol extensions would be the kind of ceremony that goes against Swift's vision of clear concise syntax in the default case. It would be like introducing a sync keyword to match the async keyword.

The beauty of protocol extensions is that users are writing generic code without even knowing it. And it is extremely rare that they need to know it. Instead, they extend Collection like they extend Array and are happy that this works.

Similarly, they will not need to know in most cases that when they write f(_ c: Collection<Int>) that they are writing generic code. They will write the syntax that they assume works (especially when coming from the many languages that have basically this exact syntax) and happily discover that it does.

The curse of P defaulting to any P today is that when they write the "natural" syntax, the syntax they learned in other languages, they are writing the thing they probably don't want – there's very few reasons to write a function that takes any instead of some, but there are more reasons to write some instead of any. It's wonderful that these two options now have parity in terms of concision, but we should go further. The more general, useful option should be easier.

Now the pushback here may be, ah, but then users may be confused, because an argument of P in Swift will not mean passing a supertype like it does in Java or Objective-C. And it's true that they differ in subtle ways. And these subtle ways are more common than when writing protocol extensions. But the first time they use the feature is not the time to teach these differences, where you will bother them with extraneous details they won't yet need to understand. That time, instead, is when they hit those differences:

func f(_ maybe: Collection<Int>?) { ... }
let a: any Collection<Int> = [1,2,3]
// error: f needs to know the exact type of `a` but it might be nil.
f(a)

// error: All return statements in f must have matching underlying types.
func g() -> Collection<Int> {
  if ... {
    return [1,2,3]
  } else {
    return Set([1,2,3])
}
5 Likes

So you've told the user there's an issue here, but it's not clear to me how the compiler will explain the difference or suggest a solution to them. A simple fixit to add any would be a possible solution, but then we've just come full circle back to having users blindly use an existential.

(I also don't understand the first error you outlined. How would I unwrap a when it's already non-Optional? Or should the user somehow unwrap the existential into a concrete type? How would they do that?)

You've made a good argument for implicit some but I think you've also outlined the strongest argument against it without fully outlining a solution.

1 Like

These are hard problems, I agree, and something we should sink some time into helping users with. But I do not infer that forcing people to write some is the solution. Instead, it's just boilerplate that does not actually explain the situation to the user, while annoying users who already know what's going on.

(I also don't understand the first error you outlined. How would I unwrap a when it's already non- Optional ? Or should the user somehow unwrap the existential into a concrete type? How would they do that?)

You're right, I wrote this too quickly. The fix is either to make the function take any, or to open the type manually. Of course, you don't always have access to the function to change it.

2 Likes

Opaque or Specific sounds fine. I just don’t think we should refer to these as placeholders.

It seems to me that the easiest way for the language to help users see the difference between some and any is to continue to require the language to enforce their usage. Otherwise you need a way for the compiler to explain, first, what the difference is, two, how the user should choose, and three, how to transform code to express their intent (ideally). Compilers are pretty bad at the first and second part and the third is unlikely to be successful without quite an investment.

So you've explained how eliding some is a good default but also why it won't be what the user will expect in some situations. Keeping some explicit will at least keep the difference visible for those users and won't require additional education from the compiler (or really, Xcode, which still doesn't surface educational notes) for users to figure out what they should be doing. The vagueness of the implicit syntax seems to be at least part of the reason Swift will move to the explicit any syntax in the first place: an implicit existential was surprising to the user in at least some contexts.

So unless this distinction can be expressed to the user in some way, it seems like requiring some isn't boilerplate but is rather load-bearing for the user's understanding. We might say this understanding isn't necessary, but you've already made a rather compelling case that it always will be.

11 Likes

This doesn’t follow for me. extension some P reads either as extending some particular concrete type that conforms to P but we don’t know the name of, or as extending all opaque types that conform to P, but not P itself. In other words, an extension on the return value of func foo() -> some P, but not on the value of let foo: any P or on instances of struct S: P.

2 Likes

There are essentially two times when these differences can be explained: when you write a function, and when they matter.

From an educational perspective, explaining the difference when the matter seems preferable. In the explicit case, when a user tries to write f(_ c: Collection<Int>) they will be presented with a fixit telling them must pick two things. That fixit is going to be very confusing to them, because it is without context. Er, I need to decide whether to what now? Take a specific type, or take a flexible type? Hmm who knows, maybe I should take the flexible type.

Whereas when they actually encounter a problem… oh you’re returning two different types here, this is when you might need any… that is clearly more educational.

When will they encounter this problem? Maybe never, but likely they will eventually. But they will probably have, by this time, been writing bare protocols for some time. So they will know that this any thing is the exception, not one of two equally valid choices the rules for which they’re a bit hazy on.

So from an educational point of view, eliding some seems far more preferable to me. But from a day to day point of view, on the other hand, by making it mandatory you have needlessly salted the syntax for those who do understand the distinction. So making it mandatory is thoroughly lose/lose.

8 Likes

Having given it a bit more thought, I do think there are some compelling reasons to avoid the elision route. I don't think they're enough to convince me it's the wrong direction, ultimately, but they do give me some pause.

To illustrate, I'll pull up one additional example that would be a somewhat inscrutable (IMO) error under the elision regime:

In the years leading up to the introduction of existential any, it was often discussed that the decision to use bare P for the existential form of the protocol type was made because it is just so attractive to be able to write a protocol type that 'looks just like' any other type. But this caused users to ignore and or neglect important differences between existential types and other types, and made those differences downright invisible at the point of use (modulo non-universal naming conventions like -able), leading us to the eventual deprecation of the bare existential syntax.

However, there are also important differences and limitations when writing generic code, so it's not obvious to me that being able to write some types in a way that 'looks just like' any other type is actually where we want to end up. Yes, automatic opening, (partially) generalized existentials, and all the other generics features in Swift 5.7 will help greatly, but there are fundamental differences between generic and concrete code—is it better for code to have those differences invisible at the point of definition?

When reading code, even if users don't grok what some means in full generality, I think it's at least become ingrained via its usage for opaque types that there's something a little funky going on that makes it not quite like a 'normal' type. In that spirit, reading a function signature like func f(x: some P) at least offers an indication for novice users that x (and f) might not behave just like any other type (and provides an easily searchable handle—"swift type some" provides good results on Google!). For more experienced users who understand some more completely, it offers a lightweight visual indication that the function is generic, without having to already know that P is a protocol as opposed to some other type.

ETA: I think this last sentence is a decent counterpoint to this argument for elision:

If there's a distinction to be understood at all, it will in some cases be invisible even to users who understand generics perfectly well. Rarely do I command-click on the types of each parameter in the signature of a function that I'm trying to understand, and I don't think we should go down a route that would require a user to do so in order to use the function intelligently.

Now, perhaps we think that the considerations when deciding to make a function generic don't rise to the level where it deserves explicit calling out at the point of definition (for both writers and readers), or maybe we think that future language evolution can close the gap enough that generic code really can be totally transparent. But I'm not fully confident that if we allow some elision in Swift 6, we won't find ourselves back here in time for Swift 7 discussing whether we should remove bare P and force users to always specify some or any.

Of course, if allowing some elision in Swift 6 really does make the any transition super painless, maybe revisiting the decision later is okay! In that hypothetical, we tried elision to soften the blow, decided it wasn't worth the costs, and so we really do need to force a mass migration to explicit some and any—but that's what we'll have to do with any anyway if we don't allow elision.

10 Likes

A similar argument could be made about let and var. We could allow the user to write x = 1 and infer that to be let x = 1 (functional language users rejoice!), and only complain when the user attempts to mutate the value. let is a perfectly safe default and the distinction between let and var only matters when mutation is introduced, so we could simply educate the user at that point. Really the only difference here is that the let / var distinction is more common than the generic / existential one (though I'm not sure what the magnitude of that difference might be).

You can make this argument about pretty much every bit of syntax Swift has, including the let / var distinction I mentioned above. Users from other languages also feel this way about Swift's insistence on marking try or await everywhere. So this argument needs to be further refined for the some case.

I do have a question for both @hborla and @Ben_Cohen: would we keep the ability to apply some in all of its use cases, or would it only be usable in scenarios where it must be available to contrast with any, like the superclass case mentioned in the first post? That is, would this be like type inference, where we can elide but provide types if we want, or like try marking, where it can only be used on actual throwing calls?

3 Likes

From the educational perspective, one of the compelling reasons for distinguishing any P from the protocol P is that it is logically absurd on its face to explain "P is not P," which we sadly have to do.

It isn't solely the distinction between some P and any P that needs to be considered here when we think of eliding some, but also the distinction between some P and the protocol P.

My hunch—and I'd guess yours also—is that in the great majority of circumstances, the possibility of confusion between some P and the protocol P is greatly reduced (or irrelevant, as illustrated by our current syntax extension P) as compared with the distinction between any P and the protocol P.

But can we be confident that there will not be scenarios in this new scheme where we will not need to write diagnostics that distinguish between some P and the corresponding protocol—the next generation, if you will, of our infamous "Self or associated type requirements" errors?

Because if it comes to that, it would be vastly preferable not to dig ourselves into a scenario where we have to say "P as written here is actually short for some P, and not bare P, and therefore P is not the P you're looking for..."

Put succinctly, I'd agree that explaining the difference closest to where it matters is preferable pedagogically, but ensuring that the difference is visible/utterable is also important.

12 Likes

This isn’t an exact analogy, because these introducers are important for more than just distinguishing these things… without an introducer at all for the “default”, we would have implicit variable declaration, which would be bad in ways that don’t apply here.

It does, however, lead to another analogy: once upon a time, you could write f(var i: Int) and i would be mutable. This was very much “let is the default, but if you want var, you state it explicitly.” Of course, this feature was removed, but for unrelated reasons (to do with people confusing it with inout IIRC).

These are also not an exact analogy. try exists to mark a possible edge out of control flow, and await a possible suspension point. They are important places to mark in the code.

A more appropriate analogy would be to say there needs to be a keyword indicating something doesn’t throw, or doesn’t suspend execution, too. But we don’t have those because these things are not symmetrically important. As some isn’t symmetrically important compared to any.

5 Likes

I am highly sympathetic to these arguments from @Ben_Cohen:

Way back in the day, I was one of the people who argued vociferously against requiring explicit self. for member variables. I wrote then, and still believe now, that anything that is widely repeated becomes invisible. Certainly that is the fate of some P if it is by far the most common syntax, as I’m hearing from the few people on this thread who’ve spent serious time with Swift 5.7.

I don’t believe that it’s possible to force comprehension by making people repeat syntax. I say this as one who spends much of his days watching beginners learn the syntax of new programming languages. Uncomprehending repetition of syntax does not spur learning; it just becomes magic incantation. As Ben wrote (emphasis added):

I’m in complete agreement. Finding the magical syntax that makes people understand the distinction up front is a fool’s errand here!

It’s not even the goal we should be asking of this syntax. I also agree with this:

Yes, indeed, that is the moment when learning occurs: when there is a problem! Learning happens best when the encounter with the problem precedes the encounter with the solution. And that is the moment where, at least for now, I tentatively disagree with Ben on whether requiring some is useful.

In that moment where somebody has written some and needs any, my instinct is that it’s very helpful for them to have a vocabulary present in the code at the point of use that they can use to talk about and reason about the problem that now faces them. “I thought I could use P! Wait, I said x is some P, therefore….”

In that crucial moment of learning, the word “some” finally does its work.

Another thing I’ve learned from teaching is that when syntax starts that leap from magic incantation to comprehension of underlying structure, students lean very heavily on the syntax they see in front of them. They look to syntax for clues first — before documentation, before web searching, even before asking the person next to them. Syntax is the handhold that lets them lift themselves up onto that high first step of learning.

In that review of implicit self, I also wrote:

Language design is all about what we choose to make implicit.

(It’s interesting to read that old review in context of this new proposal.)

I’m concerned that this is a case where explicitness is not merely redundant, but helps with concept formation and exploratory reasoning in the moment of encountering a problem. Having some types be implicitly some seems like an awful barrier to throw up to learners here — especially since opaque types are a fairly novel concept in today’s language landscape, and especially especially if the implicit some/any varies situationally! Thinking of some P as a single thought that’s always spelled out as such just makes it so much easier to talk about this stuff (with others or especially with one’s self). Shortcuts that carry cognitive load aren’t really shortcuts at all.

I’m not dead set on thinking all that — it’s only an instinct — but I do believe those are the terms on which we should judge this proposal.

(And, just to repeat again, spending more time hands on with Swift 5.7 might switch on a light bulb here! I’m trying hard not to let my thinking calcify, and I encourage others to do the same.)

10 Likes

Of course they're inexact, they're analogies. :slight_smile:

But to narrow down a particular case, my reference to try was more about Swift's requirement to mark sites that are already in a throwing context. Most other languages using try / catch don't require that, so to many users it's redundant. Swift chose to always require the keyword in every case precisely because it was thought the extra information provided by the repeated use would more clearly illuminate the control flow you mentioned, avoiding types of bugs seen in languages which don't. It requires the user to become educated immediately, even if they're in a context that would propagate those errors for them, or if they don't care about the error at all. (This doesn't even cover the confusion users from other language have over how errors in Swift work and how they're different from exceptions.)

To flip this discussion around, could you (or @hborla) summarize why implicit some is the proper default, beyond "it's not useful"? Or is it just that it's not a useful distinction most of the time? Or it it just the inverse of the argument for adding any? So far most of this discussion has been around the keyword and not the feature itself.

5 Likes

Code is read more than it is written. In languages like C#, interfaces have to start with the letter I as a naming convention to indicate to the reader that the type is an interface. Having some or any in the type indicates to the reader that we are dealing with a protocol specialty when we are mixing generics syntax.

Fix its can suggest to try using some if unsure. I feel like we gained so much readability just to loose it again.

8 Likes

I agree with the idea to elide ‘some’ is Swift 6. To me it needs a bit of a shift in my internal modelling of generics, but that’s just part of the continued push to make Swift a pleasure to use it’s most powerful features and entice developers to reach for the right tool for the job.

I think there is generally more concern right now as the wider community hasn’t used the 5.7 in anger much yet. If this idea had been floated 6 months from now I think think the reaction would be different (assuming that there is a Swift 5.8 before Swift 6 this time next year).

1 Like