Protocol<.AssocType == T> shorthand for combined protocol and associated type constraints without naming the constrained type

Fixed, thanks!

Mmm, seems like we'd be duplicating features then, or at least syntax?

Based on your analogy, I occurs to me that we could try to reuse notations instead of some/opaque:

f<T: 
P /* opaque to callee */>(x: T) -> <U: Q /* opaque to caller */> U

Associated type constraints would fall naturally out of that notation:

f<T: FixedWidthInteger>(x: T) -> <U: Collection> U where U.Element == T

You could, and that's effectively literally what a function with an opaque result does. But much like traditional generic notation for arguments, I think there's benefit to notation that reduces the number of things you need to name.

Are we concerned that people might assume that foo() -> some P works the same way as foo(_: some P)—that is, that it allows the caller to choose the type of P—and that this would therefore be confusing?

I agree that naming is hard, but here what we're working with are effectively "internal" names (as by analogy to the distinction between parameter labels and argument names).

What I see in the review thread is pervasive worry that by being unnameable there's going to be difficulty with more complex constraints, or in reusing opaque types for multiple return values. And here we are having a whole thread about inventing new syntax to avoid it.

IMO, there is simplicity in allowing users to name these things, and if it's not a meaningful thing to name, we have plenty of precedent to use T, U, and V.

1 Like

Rust and Swift are both powerful and expressive languages, but what really makes Swift stand out for me is how remarkably well is it syntactically designed to resemble human language; the tenet to prefer clarity over brevity while still capable of being relatively succinct in most cases. I hold great respect for the beautiful minds that envisioned and shaped the language to its current state. Hitherto, these design patterns have stood firm and won many hearts. @Joe_Groff, the generic dialect you suggest may happen to be a great longstanding approach for, obviously, Rust, and other more "cryptic" languages like Haskell, but for Swift it feels more like a barrier to future possibilities. I believe where clauses can handle it by a wide margin and continue to boost the expressiveness of the language, allowing generic entities to freely interact within a single list that can always be formatted and arranged to make the best sense.

Support for naming type variables is what enables us to list requirements, disambiguate between them and also use the type variable itself in the where clause.

func foo() opaque<C: Collection> -> C where C.Element == C 

The compiler does not support conditional conformance requirements yet, but that might happen in the future. A where clause copes perfectly with this task. Splitting the wordiness into smaller chunks means, well... a lot of diagnostics to ensure they remain sufficiently "self-contained", especially if we end up not trading flexibility for type anonymity.

func foo<T: Collection>() opaque<C: Collection> -> C
  where T: Equatable when C.Element: Equatable
2 Likes

I'm certainly concerned about readability. If we expect users to handle Array<Int> already, though, then I don't think it's a stretch to expect them to also understand Collection<.Element == Int>, and if anything, that's a smaller conceptual leap than is currently required to generalize from concrete types to associated-type-parameterized generic implementations. We very commonly get questions about "why isn't it just Collection<Int>", after all. My hope here is to get to a place that is both clearer and more concise than the current syntax allows.

3 Likes

One thing to consider is that, in many contexts, even if a type isn't directly namable, there is a closely-related value or other declaration that does have a name. For instance, properties or arguments of opaque/existential type have binding names for the values already. For a property let x: some P, it might be nice to be able to just refer to its type as type(of: x) instead of having to introduce a new name just for the independently-irrelevant type.

+1

I'm worried, like some, about adding angle brackets in more places… My head goes to being able to give the opaque type a name that's local to the function declaration, like in my second example here:

// current
func doSomethingWithACollection<C: Collection>(_ c: C) -> C.Element?
    where C.Element: Comparable, C.Element: Hashable

// with named opaque return type
func makeMeACollection<E>(_ e: E) -> some C: Collection
    where C.Element == E, E: Comparable & Hashable

This keeps all the where clause business in one place, and extends to things like tuples of opaque types, etc.

1 Like

Why is adding angle brackets a problem for opaque types and existentials if it isn't a problem for concrete types?

I have a strange feeling this particular syntax doesn't fit Swift, but I don't have a better anonymous alternative to offer. I wouldn't be against if the shorthand doesn't prevent us from naming types when we have to, though I do predict an avalanche of "why can't I do this?":

func foo(arg: Array<.Element: Proto>) { ... }

I'm not sure what syntax would be best, but I don't think there is anything inherently bad about generic types having existentials.

For me it's the combo of the angle brackets and what's inside. The related stance we've taken already is to move where clauses out of the generic parameter brackets (in SE-0081). That lowers the level of complexity for someone trying to read the function declaration.

Having a name for the opaque type that's local to the declaration helps with the constraints, too. I think the _.Element and .Element syntaxes are difficult to explain, since they literally leave out the thing that they're attached to.

A function declaration using a named opaque type breaks neatly in half, so you can first process the shape of the function and the generic/opaque parameters it uses (1), and then look at the constraints in the where clause (2):

/* 1 */ func makeMeACollection<E>(_ e: E) -> some C
/* 2 */     where C: Collection, C.Element == E, E: Comparable & Hashable
2 Likes

One could imagine an alternate language design path where generic types used associated types like protocols do, so that you had to constrain them to get to a concrete instance.

1 Like

It was actually meant to be someone's intention to write

func foo<T: Proto>(arg: Array<T>) { ... }

:slightly_smiling_face:

Gotcha. In that case, I think what you meant was something like this:

func foo(arg: Array<some .Element: Proto>) { ... }

In that syntax Array is concrete but its Element is opaque with known constraints. I also don't see an inherent problem with this. It feels like a reasonable generalization of the syntax @Joe_Groff has pitched.

No, I really meant people trying to use Array<.Element: Proto> exactly as if it were Array<T> where T: Proto.

You probably meant that Element is a generalized existential, otherwise it would always have to be the same concrete type, though opaque, correct? My main concern remains that Collection<Int> sounds very tempting... right until powerful Swift constraints come in.

func foo(arg: Array<some .Element: Collection<.Element == ..Element>>)

No, I was inventing a new form of the opaque argument types @Joe_Groff has mentioned as a future enhancement. Instead of the entire type being opaque, only some of its type arguments would be opaque. This is a generalization of the request for opaque types to support Optional.

I'm not sure what ..Element is supposed to be referencing here. Is this supposed to be an Array with Element of an opaque Collection whose elements are values of the same opaque Collection? That sounds pretty strange so I'm not sure it's what you meant but I can't make any other sense of that syntax.

Yes, never mind, I just realized it can be written the other way around.

Array<some .Element: Collection, .Element == .Element.Element>