[Pitch] Opaque parameter types

Hi all,

Opaque result types have been part of the language for a while now. Ever since the early pitches, we've also talked about using some in parameter positions, which came up again in the recent thread on easing the learning curve for introducing generic parameters. In essence, opaque parameter types allow the use of some P within a parameter type as syntactic sugar for an implicit, unnamed generic parameter. So something like this:

func horizontal(_ v1: some View, _ v2: some View) -> some View {
  HStack {
    v1
    v2
  }
}

is syntactic sugar for

func horizontal<V1: View, V2: View>(_ v1: V1, _ v2: V2) -> some View {
  HStack {
    v1
    v2
  }
}

I now have a proposal describing this language extension along with a complete implementation and toolchains (Linux, macOS) to experiment with.

Doug

61 Likes

Thank you Doug! It may be time to quote myself:

1 Like

Not much to say other than +1.

1 Like

The proposal looks great, just one small typo:

func takeStrings(_: some CollectionString>) { ... }

There's a missing <.

Love it, +1

+1

+1, definitely.

Like it.

Can I assign one of these to a local variable? with inferred type? with an explicit type annotation?

func foo(a: some T) {
  let b = a          # Does this work? I assume so…
  let c: some T = a  # How about this? I think not?
  let d: any T = a   # How about this? I assume it does, but…but what?
}

If there is a need to leap into explicit generics:

func foo<SomeT: T>(a: SomeT) {
  let c: SomeT = a
}

…then how do we help programmers discover this? That’s a hell of a leap there for somebody who doesn’t have the whole back story.

5 Likes

Fixed, thanks!

Yes, their motivation section is well-thought-out, and many of the arguments map precisely to Swift because Rust and Swift have very similar generics systems in this regard. It's probably worth paraphrasing their arguments and linking to that RFC, with a big disclaimed that Rust uses the term "existential" very differently, which causes endless confusion.

Doug

4 Likes

I like it. Generally I like the idea of using "some" for generics and "any" for existentials.

Will it support more than one protocol? (Edit: or no protocols, see below)

func foo<T: Proto1 & Proto2>(v: T) {}
----->
func foo(v: some Proto1 & Proto2) {}

As an alternative, I was thinking along the lines of sugaring:

func foo<Apple: Comparable & Identifiable, Orange>(a: Apple, b: Apple, c: Orange) {}
----->
func foo(a: Comparable & Identifiable Apple, b: Apple, c: generic Orange) {}

which is a bit more general in that it allows using the same type for more than one parameter without resorting to the full form.

Edit: comparing a few alternative forms (unconstraint type case):

func foo<Apple>(v: Apple) {} // full form
func foo(v: some) {} // Doug's version. is that allowed?
func foo(v: generic Apple) // alternative form
func foo(v: some Apple) // alternative spelling in alternative form

+1

I'd even like to see us drop the some, but perhaps the lesson from our current existential syntax is that neither generics nor existentials should be spelled using only the type name. some vs any gives us a nice language model that is also explicit.

10 Likes

Minor, the link in "Introduction" section is wrong:

Yes, and b has the same type as a.

c has some opaque type, which we only know is "conforming to T", but whose underlying type is the same as a.

d can hold any type that conforms to T. At the point of its initialization, it dynamically holds something that is the type of a.

This isn't the same as your first one, because every utterance of some T is a unique opaque type.

A refactoring action that took a single utterance of some P and turned it into an explicit generic type parameter would affect only the signature of your function (not the body), and everything else would work the same:

func foo<SomeT: T>(a: SomeT) {
  let b = a
  let c: some T = a
  let d: any T = a
}
}

Yes, absolutely! I'll clarify this in the proposal document, thanks.

[EDIT: this was already in the document, with this example:

func encodeAnyDictionaryOfPairs(_ dict: [some Hashable & Codable: Pair<some Codable, some Codable>]) -> Data

]

I really think that, if you're going to be introducing a new for the generic type parameter so that you can use it in multiple places, the generic parameter list is the right place for it.

No, this would be written some Any, just like we have the ability to write any Any with SE-0335.

This proposal is a necessary step to doing that, but there are a lot of other considerations as well. Can we keep that discussion separate?

Ah, I pushed the proposal before posting the thread here. I'll go update the links.

Doug

8 Likes

Love it!

Worth noting how nicely this syntax would evolve to support default generics as well:

func horizontal(
  _ v1: some View,
  _ v2: some View,
  _ v3: some View,
  separator: some View = Divider()
) -> some View {
  HStack {
    v1
    separator
    v2
    separator
    v3
  }
}

That's an orthogonal feature of course, but it's so good-looking that it might be worth an entry in "Future Directions" :slightly_smiling_face:

3 Likes

Great pitch. One other future direction worth exploring would be the ability to further constrain the opaque parameter types.

Bikeshedding syntax:

func foo(v1: some View) where v1.Type: MyProtocol

I'm also grateful that this pitch did not included the some Sequence<...> feature and kept it in future direction as it definitely will be a very hot topic. I personally strongly remain against dropping the explicit associated type and mixing it with pseudo generic protocols.

1 Like

What’s different between that and just using some View & MyProtocol?

None, but that example is probably oversimplified. There are definitely other use cases where a where clause would be the right extension for this type of feature.

Isn't that just this?

func foo(v1: some View & MyProtocol)

Perhaps you mean some more complex example:

func foo<T: View>(x: T) where T.Body: Equatable {}
func bar<T: RawRepresentable>(x: T) where T.RawValue: Comparable {}
1 Like

Okay I have to express a bit of negative impression. So the proposal puts the Protocol<...> feature into the "Future direction" bucket. Yet it already implements this under the hood?! Please DO NOT make this feature available in the public without a proper evolution process. I'd be highly disappointed to see that move.

Sure the feature is hiding behind the private @_primaryAssociatedType attribute, but it's not hard to find it and actually start using it. As soon as someone used it people start arguing that we have to keep it because there is code that relies on it. That's not a great way to sneak this feature in.

The "primary associated type" feature is implemented behind an experimental frontend flag, just like the current implementation of opaque parameters has been merged behind a different experimental frontend flag. Both features have been pitched separately, but yes, there are test cases that use these experimental features together. Each feature will, of course, go through the Swift Evolution process.

No features are being "sneaked in". You cannot use @_primaryAssociatedType without also enabling the experimental compiler flag.

9 Likes