[Discussion] Eliding `some` in Swift 6

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.

10 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?

2 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

This is for me the strongest argument against this pitch. When the return type is simply Collection, everyone will assume that as long as you return a Collection, it will be fine. The fact that it doesn't work will be baffling.

11 Likes

Couldn't this be made to work though, with the type of f being (some P) -> (), or would that be impossible?

Since distinction between "protocol as constraint" and "protocol as a type" isn't going to go away, the only thing that can be done with it is either hide it in the syntax (as was before "any" keyword) and cause confusion when developers get hit by errors, or make it explicit (with currently accepted proposals using explicit any and some), and by making explicit allowing developers to understand the difference and use them correctly. Even now, every month people get confused by the implications of these concepts and I have hard time understanding how making the feature magical again (via elision) would somehow be better for the developers in the long run. Swift isn't meant to be just a power-user language, after all.

To me, it's critically important for all developers to understand that "bare P" is not the same thing as "some P" or "any P", even if they are still learning about the differences between some and any.

A (rather harsh) analogy to be would be removing "let" and "var" from swift language and making compiler decide whether a variable should be constant or not. Sure it would save typing a few characters, but it also takes away control from the developer and instead compiler is the one in control, doing magic that is inscrutable to the average developer.

Since there are both subtle behavioural as well as performance differences between "any" and "some", similarly to "let" and "var", the high performance code, at the hands of a skilled developer, would anyway need explicit control over what is actually happening in the code. So would you then have elision most of the time, but then still allow explicit keywords sometimes? What if developer A does highly performant code (explicit some/any) and then developer B, used to the elision (as the default), removes all the performance optimisations by accident, when he doesn't know anything about any/some and removes those keywords from the codebase since "it compiles just fine and now code it looks cleaner"?

I understand the desire to make things simple for novice developers, but I think it's derogatory to those developers to make them ignorant and stupid, which is the situation with elision, when they don't even understand the code that they are writing. As mentioned by others it also makes the error messages almost as inscrutable as was before "any" since developers won't know what any/some actually means, and they don't know the difference between "P" and "some P" and "any P". So instead of a learning curve, they get hit by a brick wall, as before.

EDIT: sorry, didn't notice @Jon_Shier had similar analogy of let/var. Oh well :slight_smile:

6 Likes

That would be equivalent to <T : P>(T) -> () and that's not possible (generic closures).

1 Like

Ah yes, I remembered that after I posted. But that could be made to work I suppose? Never understood why I doesn’t.

As much as I don't like the removal of that feature, the decision, mirrored with capture lists, demonstrates that the language designers understood what people want nearly all of the time (let).

Implicit some demonstrates the same. But nobody is suggesting an annoying verbose workaround when choosing any like we do when choosing var.

final class C {
  var property = 0
  
  func function() -> Int {
    let renamed = property,
        property = property,
        betterNameThanFive = 5

    return property + betterNameThanFive
  }

  var closure: () -> Int { {
    // The braces can only impose an implicit `let`,
    // matching the function above.
    // `var` is not an option.
    // `property` instead of `property = property` is appreciated though. 🙂
    [ renamed = property,
      property,
      betterNameThanFive = 5
    ] in

    property + betterNameThanFive
  } }
}

I’m not sure I understand what’s going on here (and I didn’t know you could do this in capture lists—that’s cool) but wouldn’t the compiler suggest your renamed = property be changed to _ = property because renamed is never used?