SE-0341: Opaque Parameter Declarations

If you never read the parameters or return type, why would you care if a function's generic? If you do read the parameters or return type, I think most would be find it easier not to have to piece together the type signatures manually, especially when the default Swift style puts the generic constraints after everything else in the definition.

Yes, people struggle with this syntax. In my experience the unique syntax of generic declarations has a steep learning curve for new and junior developers. Easing the ramp into that feature by reusing the opaque syntax should make it more understandable for users who don't yet fully grok the capabilities of the current generics feature. If you tell someone to "save an encodable value to disk", which is the more natural signature? func save<Value: Encodable>(_ value: Value) throws or func save(_ value: some Encodable) throws? Which is easier to teach? Which is easier to remember? For most people it would be the version that' looks more like English and which reuses syntax they've see used elsewhere, like body: some View.

3 Likes

Sure, the some might be simpler your first time encountering it. But now you've created two things they need to learn and remember, so there's more conceptual overhead overall.

1 Like

Not really, given they'll need to learn the some syntax anyway. And most users never progress beyond simple generics, so it's not like they have to learn the more complex version in the first place. Or if they do, they simply need to recognize it in other signatures, not write it themselves. And once they need the more advanced syntax they're at a much better place to understand both it and the fact that the some syntax sugars over it, due to the more gradual introduction they received.

2 Likes

Learning about some in the return position doesn't teach you anything about generic functions though. It can't be re-written with a type variable inside of angle brackets, where as some in parameter positions can. It's just a single, concrete type that's hidden from the caller; there's no conceptual overlap with functions that are actually generic (for this reason I think "reverse generics" is a misnomer, opaque return types is a much more apt name).

This goes back to my point that some is becoming overloaded to mean different things in different contexts, which only increases the learning curve even more. We're conflating things that are not at all similar and expecting the difference to be intuitive.

2 Likes

By the way, this is a good example that doesn't involve SwiftUI so I redact my statement about it mainly benefiting those use cases. I was also incorrect to say that this feature serves unconstrained generics. The generic type here is constrained to Encodable. The proposed syntax just can't constrain against associated types, which admittedly is less common but I do think Swift users will need to understand them.

For example, I think the average Swift user will need to be able understand append(contentsOf:), even if they don't write such functions themselves. This is a really commonly used method; associated type constraints are not limited to advanced or archaic programming patterns.

1 Like

Along with the lightweight constraint syntax, this proposal brings append(contentsOf:) to:

extension RangeReplaceableCollection {
  mutating func append(contentsOf newElements: some Sequence<Element>) 
}

... and it's pretty hard for me to imagine something more clear or minimal than that. We just want to take some sequence of our own element type.

Doug

13 Likes

Thanks Doug. Yes, if both proposals are accepted then the need to declare type variables up front is drastically reduced and many users can get by just knowing about some.

If the lightweight syntax is not accepted then I think this pitch is less valuable, as it will increase the learning curve of generics by having multiple ways to do the same thing. In your opinion, is this proposal contingent on the lightweight syntax or should they be considered in isolation?

Pat

Users don't need to know how the signature of append work to use the API. Their natural understanding of append is enough to understand what the method is supposed to do and what the result will be. Usually the only part they're thrown off by is the different between append and `appending. Beginners, and probably most users in general, will never look at the generic signature of a function. They just expect it to work. So I don't think your concern here is based on how users actually behave and rather overstated.

1 Like

Sure, I think it's fair to say that append is intuitive enough and maybe folks won't need to read the function signature. And to Doug's point, if the light weight syntax is also adopted and if the entire standard lib API is migrated, that wouldn't be an issue. If.

However, let's also think about every generic function in every Swift codebase to date that currently use angle bracket syntax. Many codebases contain functions like your example:

func save<Value: Encodable>(_ value: Value) throws

Refactoring these functions to use some will be a gradual process. Some function signatures may never be migrated, primarily because the function already works and there's not some immediate and overwhelming benefit to changing it. Even if the standard lib is migrated to some params on day 1, Swift users will be encountering type variables in angle brackets for years to come in both proprietary and open source codebases, and so they'll still need to learn both. Everyone updating all of their generic functions creates churn, and the mixed state that will exist in the interim (or forever) will be confusing and require more learning for these beginners that we claim to be helping.

Let's also keep in mind that not every programmer new to Swift is a beginner programmer. Many are coming from languages that make heavy use of type variables in angle brackets, Java, C++, C#, Kotlin, etc. some will make Swift generics less familiar and intuitive to this crowd, without any benefit to expressiveness or performance. It doesn't make Swift generics more powerful, just less approachable.

The point also remains that some now means different things in different contexts. My overarching point here is that it's not obvious, to me at least, that the learning curve is being improved at all. It's break even at best.

4 Likes

The protocol requirement and default implementation are written as:

  mutating func append<S: Sequence>(contentsOf newElements: __owned S)
    where S.Element == Element

The generated interface and documentation are translated into:

  mutating func append<S>(contentsOf newElements: S)
    where S : Sequence, Self.Element == S.Element

The constraint written in angle brackets has moved to the where clause.

Could the tooling automatically generate the simplified interface you posted?

(We would still want to gradually refactor code to use the improved syntax.)

4 Likes

Recall that Xcode already fibs in autocomplete lists, showing methods like this (misleadingly IMO) as append(contentsOf: Sequence). Having an actual abbreviated syntax would allow this to be spelled correctly and succinctly.

8 Likes

I'd like to drill into this point with a specific example of how this will be confusing. With this proposal, some P takes on a new meaning that does not overlap with opaque return types and is now context dependent. Imagine the following two function declarations:

// opaque return type, known only to the callee
func makeSomething() -> some P
// sugar for a type variable that you'd normally find in angle brackets
func takeSomething(value: some P) 

Depending on which of these functions a user encounters first, they'll decide on what they think some P means, but later they'll bump their head and realize that it actually depends on the context.

If they encounter makeSomething first, they'll decide that some P means a single, concrete, callee-specified type. When they later encounter takeSomething, they'll realize this can't be true as the callee doesn't construct argument values, so can't possibly be the one to specify their types. some P must mean something different here, more learning is required.

If they encounter takeSomething first, they'll decide that some P de-sugars to a type variable in the function declaration, as if it were written func takeSomething<T: P>(value: T). Later when they encounter makeSomething, they might assume that they could write something like:

let value: MyType = makeSomething()

Which won't compile because some P in the return position does not de-sugar to a type variable at all. It's not the same as writing func makeSomething<T: P>() -> T. Head == bonked and queue more learning.

So the idea that users "will need to learn the some syntax anyways" is false. They'll need to learn two different features which, very unfortunately, share the same keyword.

Putting myself in the shoes of someone who is new to Swift, I think my head would be swimming at this point. Then I inevitably encounter angle bracket type variables (see previous post, they're not going away nor are they "advanced") and I've got to learn that too?? I don't think we're doing them any favors with this proposal.

Side note about the name of the proposal

I think that "Opaque Parameter Declarations" is a misnomer. Generic arguments types are already opaque to the callee, sugaring from <T: P>(value: T) to value: some P does nothing to make the type of value more opaque than it already was. A more appropriate name might be Anonymous Argument Type Variables, because all we're doing here is obviating the need to name a generic type before you can use it (but only for arguments, not for a return type, because that would clash with opaque return types and will never be possible using the word some).

5 Likes

+1. I don't have anything to add to the discussion except for my comment on "Light-weight same-type requirement syntax" pitch.

Regarding this comment from your other post:

“I believe Swift will become the language that introduces the majority of new programmers to generic programming in a few years.”

I’m genuinely curious, what’s the justification for this belief?

There's something in this reasoning. Should we use different names for different concepts? For example this or a better alternative:

func makeSomething() -> some P
func takeSomething(value: generic P) 
3 Likes

I don't feel like these are two separate concepts at all. In both cases, some P means some specific kind of P; you just don't know what it is. That's true in the parameter position, and it's true in the return position.

You can describe some P in return position as "reverse generics" if it helps you understand the concept, but that is not the official description of the feature. It's an "opaque type"; some specific type that conforms to P, you just don't know exactly what it is.

You can describe some P in parameter position as "generic" if it helps you understand the concept, but you don't have to understand generics to use it. It's exactly the same concept as in return position; when you receive it, it's some specific type of P; you just don't know exactly what it is.

I don't see why the "callee-constructed"-ness of the type would be understood as essential to some here. The name some doesn't suggest "callee-constructed". I would think that the callee-constructed-ness of it would be inferred not by the keyword some, but rather by the fact that the value is presented as a return type, and the return type comes from the callee, so the callee must have constructed it.

They'll only decide this if they're already familiar with generics. I view this feature as a nice step in Swift's philosophy of progressive disclosure. Most users a few years from now will likely encounter func f1(p: some P) long before they encounter func f2<T: P>(t: T). If that's the case, then users wouldn't immediately start by seeing some P as a sugar for generics—rather, generics would be seen as a more advanced version of opaque parameter types, to be learned later.

In short, I like this proposal. +1.

10 Likes

I replied to the wrong address. This is a reply to @bjhomer.

I am one of people who feel "callee-constructed"-ness is essential to some. In my case, this feeling comes from the strangeness to have two different equal-ness in the same language.
As we all know, generic parameters and generic result types are similar features. Then, generic parameters and 'reverse-generic' result types will seem to be similar features, because they share the same some syntax.
As a result, we will have two different identity relating to generics in Swift, if we introduce proposed feature. One is whether it is generic or reverse-generic, and the other is whether it is some or not. We don't usually need two different viewpoints, but there are two.
If I consider "callee-constructed"-ness is essential to some, then there is only one sameness in Swift. This is much more natural for me.

1 Like

When users learn that the advanced angle bracket syntax can be used for some parameter types they would try to apply that advanced angle bracket syntax for some result type and would be confused why they can't.

1 Like

I somehow got reminded on something that has been posted long ago:

Staying in this analogy, I lately got a strong feeling that there are some bricks which Core does not like anymore, but instead of searching or creating new stones, they try to reshape everything with lots of mortar…

I'm not opposing this particular proposal and don't think it will change Swift significantly, but small things add up, and it looks unavoidable that the language becomes more and more complex and less elegant :-(.

2 Likes

My justification will need a full sized article and in the end, it will be just my opinion. Tbh, I don't have time to get into this discussion. Let's assume it is my gut feeling without justification and leave it at that.

1 Like