SE-0328: Structural opaque result types

Parameter declaration is the key phrase. I specifically meant this (which, to be clear, is just a future direction):

protocol P {}
struct S: P {}

func generic(value: some P) { ... } // equivalent to `generic<T: P>(value: T)`

generic(value: S()) // implicit type parameter `some P` is substituted with `S`

Whenever you have an expression that contains some P as a parameter type, e.g. (some P) -> Void the type parameter has already been substituted somewhere else.

1 Like

It may not muddle that specific formulation of the rule, but it's also the case that the behavior today, and even moreso under SE-0328, satisfies the rule:

If some P appears anywhere in a declaration, the underlying type for that opaque type is chosen by the author of the declaration.

This formulation of the rule would be violated if some P came to mean a caller-provided type parameter.

IMO, it would be highly confusing for some P in parameter position to mean two different things. If we think we would (or might) want some P to signify an implicit caller-side type parameter, then I think we should disallow some P in parameter position as part of SE-0328. If we're not ready to decide that question, then I agree with @xwu that we should simply disallow it for now and revisit once we are ready to discuss that issue in a targeted manner.

4 Likes

Yes, I agree with this assessment.

It's really important to make a distinction between "parameter position" in a function declaration vs function type. This proposal doesn't allow opaque types in parameter declarations, so that's fine, we can debate that later :slightly_smiling_face: If we explicitly disallow function types having opaque types in parameter position, that's a source breaking change that I don't think we should make as part of this proposal (it's possible for such a type to be inferred today as @Zollerboy1 demonstrates above, but spelling out this type in source is currently an error)

1 Like

This is my feeling as well.

However, if I am given a new rule that is as simple to understand as if some P appears anywhere in a declaration, the underlying type for that opaque type is chosen by the author of the declaration, then I'm willing to upgrade myself.

1 Like

Yeah, I get the distinction you've drawn between parameter declarations and parameter types, but I worry this is too subtle a distinction to introduce into the language model. If I'm understanding correctly it would make:

let f: (some P) -> Int = { ... }

and

func f(_: some P) -> Int { ... }

behave very differently with regards to generics, which, IMO, would be very undesirable.

Yeah, I'm specifically talking about the "spelling out this type in source" part. Existing situations where some P can sneak into a parameter type are pretty arcane, as evidenced by the many experienced Swift programmers here who didn't even know it was possible, myself included until a couple weeks ago!

Right now, I think it would be very difficult for users to even see this type exposed as some P. Not only would they have to perform the proper incantation to construct such a type, but even when inspecting the type of such a function, the code completion window only displays it as (in the example above) (BinaryInteger) -> Bool. An improper use of the function does result in the error:

Cannot convert value of type 'Int' to expected argument type 'some BinaryInteger'

In any case, I think that allowing users to spell some P in parameter position is a significant step, and one that there are clearly open design questions around. I suppose that the acceptance of this proposal wouldn't formally prevent the caller-side some P for parameter declarations, but I want to make it clear that I consider the current form of SE-0328 and some P as caller-side generic shorthand pretty much mutually exclusive from an ergonomic perspective.

If we accept this proposal I would argue pretty vehemently against a future proposal to imbue some P with caller-side semantics (though I'm open to being convinced that I'm overreacting :slight_smile:)

3 Likes

I think what @Jumhyn is agreeing with—if I may be so bold—is my feedback that we should decline as part of this proposal to make it not an error (as it is currently) to spell opaque types out in parameter position. Not making a change to Swift is, by construction, not source-breaking.

How Swift’s diagnostics currently display an inferred type for users to read can also be workshopped later if necessary, and I wouldn’t call such changes source breaking.

2 Likes

Right—I'm not suggesting that we adopt any source breaking behavior, only that we not be too permissive with the new spellings this proposal will allow, given that there are open questions about how such spellings might interact with potential future proposals.

1 Like

And, for clarity, neither am I in my feedback suggesting that we should disallow the inference of function types that we currently infer; this proposal is about (at least by my read) what and how we spell and my feedback is similarly about only that.

I absolutely agree that there is no reason to disallow the underlying types themselves of these usually uncallable closures—and indeed in my feedback I state explicitly that in the fullness of time we should be able to make them utterable, just perhaps not now and not with this syntax.

2 Likes

Without strongly taking a side, I’ll note that the Rust folks must have already discussed this when deciding to allow impl Trait in parameter position. RFC: Finalize syntax and parameter scoping for `impl Trait`, while expanding it to arguments by aturon · Pull Request #1951 · rust-lang/rfcs · GitHub

4 Likes

This is fantastic—thanks @jrose.

To put it succinctly then, I (and some others) are advocating for some Protocol in Swift to work exactly like impl Trait in Rust, with the arguments exactly as stated in that document. Others are advocating for it to work like the mooted some Trait in Rust that’s been considered and not adopted.

Finally, @hborla, if I understand her correctly, is advocating for the same syntax in Swift to work like some Trait in a function type but like impl Trait in a function declaration. (All of this advocating on all parts being part of this discussion only insofar as it impacts what we adopt or don’t adopt at this juncture.)

Whatever one’s take on this issue, given that there is a divergence in opinion, I think it’s pretty reasonable that this topic merits its own discussion and we ought not to foreclose any one of these options at this time, which would occur if we adopt this proposal as-is.

6 Likes

Generally, I'm a fan of making a language feature more restrictive to start under the assumption that it's much easier to remove limitations later, but in this case, I don't totally understand what it buys us. I understand that we haven't yet figured out what some P might mean on a parameter declaration, but I don't see how that would cause us to change how this proposal behaves. If we decide that some P on a parameter declaration should mean an implicit type parameter that's inferred from the value provided at the call-site, I do not see how that would cause us to change the behavior of the type (some P) -> Void to mean an unbound type parameter, because that's an entirely different feature. Swift does not allow any free type variables in expressions today at all, and deferring type variable binding across expressions is something that would affect a regular type parameter T too. I see this as a much bigger feature that, if we think is valuable, should be considered for all type parameter, not just some P.

Further, given that the type some P in a parameter position already exists today in expressions, the only thing I could see us doing is changing the syntax of this type, which in my opinion, doesn't make sense. It would make more sense to spell some P differently on parameter declarations (which I personally don't think is necessary, but this is the thing I think we can debate later :slightly_smiling_face:)

If you see a different possibility that I'm totally missing, please let me know!

I'm really curious to hear more about why you think that's the case. Would you mind elaborating? To me the semantics make sense, I'm sure that's partially because I work on the compiler - I realize that most Swift programmers don't need to make the distinction between a type parameter and an archetype.

I need to take the time to read through the linked proposal a little more carefully, but I'll comment back on whether this is the case after I do that :slightly_smiling_face:

3 Likes

Sure! I think my objection is pretty much summarized by this example:

Visually, these two declarations appear very similar. Were I coming to the language without the benefit of these discussions around opaque types, I would expect them to behave (near) identically, especially because the existing return-type equivalents:

let x: some P = ...
func x() -> some P = ...

mean basically the same thing. We'd also have a situation where (some P) -> Int is a valid type, but the type of func f(_: some P) -> Int is not, in fact (some P) -> Int. In fact, users can't even write the type of func f(_: some P) -> Int in source.

Were we to adopt the behavior that treated

func f(_: some P) -> Int { ... }

as equivalent to

func f<T>(_: T) -> Int { ... }

I could probably learn to internalize that rule, and reason through why that interpretation would make no sense for closure types, where generic parameters are disallowed, but I would consider it very confusing.

If we (I?) ever get around to allowing compound names for closures, and adopt the sugar that specifies the parameter name inline with the closure type, the closure version of f would look even more similar to the function:

let f: (x: some P) -> Int = { ... }
func f(x: some P) -> Int { ... }

I would be disappointed if adopting these proposals for extension of some provided a reason to avoid the inline specification of parameter names for closures.

Similarly, if we ever did adopt a feature that allowed for generic closures to be specified, the reasoning for the difference between let f and func f above would no longer be available. The difference between some P in a function parameter and some P in a closure parameter would only be explainable as a historical oddity of the some syntax in Swift, unless we considered the problem at that point large enough to justify a source break.

Now, it may be the case that independently of anything related to some syntax, we wouldn't want to adopt either of generic closures or inline compound name syntax for closures. But I don't think either of those features have been sufficiently explored, and I would hate for us to box ourselves in by adopting syntax prematurely. And, even without considering future evolution, I consider the two different meanings of some P in parameter position to be undesirable for the reasons already discussed.

I think we should draw a distinction between opaque types (or "reverse generics") and types using the some syntax. In the language today, some syntax only exists as a top-level property type and a function return type (I believe?). No user has written some P to indicate an opaque type in parameter position, and as far as I have been able to tell we only even expose some P in parameter position via diagnostics that are relatively difficult to access. So, in terms of syntax we wouldn't need to change anything—in the language today there is no syntax that spells an opaque type in parameter position.

The only thing we would need to change is what we call an opaque type in parameter position when communicating with the user, given that there's no source spelling for such a type. I doubt many users have encountered the existing diagnostic so I don't think it would be particularly confusing to change.

And, to the extend that it would be confusing for users to change the spelling in diagnostics, I think it would be at least as confusing confusing to introduce another meaning for some P in parameter position.

This may have been a bit disjointed but hopefully that makes my thinking a bit clearer about why I consider some P meaning different things in parameter declarations and parameter types an unacceptable outcome!

6 Likes

I need to take some more time to process your message (thank you for elaborating!), but I wanted to respond to this point first because it isn't entirely true. If a type containing some P in parameter position can be inferred, this also means it can appear in QuickHelp and other tooling that can surface inferred types, including generated Swift interfaces. This is what I meant by "change" - I understand that nobody can write this type today manually, but inferred types still have a spelling, and that spelling today uses some P in parameter position (if you want to test this out yourself, paste the small example with some BinaryInteger from upthread into Xcode and invoke QuickHelp on baz or view the generated interface).

EDIT: This isn't to say that we can't change it, just that it is surfaced to users in more cases than diagnostics.

2 Likes

Well, kind of like how __shared and their ilk can appear on generated interfaces, etc., no?

Insofar as this hasn’t gone through Evolution, though, it’s not officially part of Swift syntax—and particularly since no one has ever been able to actually write it in source and therefore any change can be entirely effected in the Swift project itself, we can deem ourselves entirely free and unconstrained to change the spelling.

By contrast, this proposal in its current state (not the status quo) would officially fix the spelling as part of the syntax of Swift such that we are no longer free and unconstrained to make such a change. The rationale for making it official cannot be that there is currently an unofficial implementation in some form. Changes to Swift’s user-facing features mustn’t be allowed to do an end run around the Evolution process by accretion.

5 Likes

This isn't at all what I was trying to say. My point was that allowing free type variables in expressions is a different, much larger feature. I did acknowledge that we can "change" the syntax of the un-utterable type that already exists/introduce a syntax that does not use some. Personally, I think the fact that the type is surfaced via Swift tools is relevant, because programmers use these tools to form a mental model of how the language works.

If syntax is the main concern, I can understand why it might be valuable to accept this proposal with the modification to not allow some P as a parameter type in higher-order functions, even if my personal opinion is that the syntax in this proposal makes sense.

3 Likes

Generated interfaces are a good point to raise, but I did at least check how this was surfaced in QuickHelp (EDIT: I was actually looking at code completion, not QuickHelp :slightly_smiling_face:)—for better or for worse, opaque types in parameter position are surfaced as just (P) -> Int (or similar). May be a bug, but also I think speaks to the fact that this is a pretty obscure emergent behavior of opaque types and not something we should worry too much about changing.

3 Likes

I guess (P) -> Int only surfaces in code completion, which uses IMHO erroneously condensed signatures in which even generic parameters are wrongly replaced with existentials for the sake of brevity (sometimes even invalid ones, or at least, invalid prior to SE-0309).

3 Likes

Ah, you’re right. I was referring to code completion rather than quick help! Thanks for the correction @xAlien95!

With SE-0328, you can create some P in parameter position easily with typealias, even if we ban bare some P in that position.

typealias Closure<T> = (T) -> ()
let closure: Closure<some Numeric> = { (x: Int) in print(x) }

typealias using type parameter more than once creates same-type constraint:

typealias Tuple<T> = (T, T)

// t0.0 and t0.1 have the same type, but t1.0 and t1.1 don't necessarily have the same type
let t0: Tuple<some Numeric> = (0, 0)   
let t1: (some Numeric, some Numeric) = (0, 0)

However, when it is only once that the type parameter is used, the behavior should be consistent with its expanded form, because there are no additional constraint. Therefore, I think it's natural to expect (some P) ->() and Closure<some P> to behave in the same way.

There are only two possible plans: whether to accept bare some P in parameter position or not. While acceptance of that is supported by the argument that the behavior of (some P) -> () should be equal to that of Closure<some P>, banning is supported only from the view of 'future direction'.
I'm not strongly opposed to ruling out bare some P in parameter position from SE-0328, because they can be added in the future. But typealias can create some P in parameter position easily, and (some P) -> () as Closure<some P> is pretty natural interpretation. Banning them seems too artificial limitation to me.

1 Like

I have followed the discussion around some P in parameter position for a while now and wanted to add my two cents to it.

For me some P indicates: This is some specific type which is chosen by the callee and I (the caller) cannot see, which type got chosen exactly.
The sugar that many people want now (namely that func foo(_ bar: some P) is equal to func foo<T: P>(_ bar: T)) doesn't fit with that description of some P at all.

I would much rather want that some P in parameter position means what it already means today (at least in diagnostics and quick help).
This would just work with closures, since we could just write:

let closure: (some BinaryInteger) -> () = { (value: Int) in
    // ...
}

to get the desired behaviour.
With normal functions however, that wouldn't work like it works with closures. Here we would need a way to tell the compiler what the actual underlying type is. I could imagine some syntax like

func foo(_ bar: some BinaryInteger) where bar: Int {}

but this feels a bit strange, because normally everything in the where clause is part of a function's signature, but here it would be not.
So maybe we would have to find a better syntax for that, but I still think that this functionality of some P in parameter position would be more consistent than that it sugars a (very simple to write) generic function.

2 Likes