Allow static extensions of generic types to be written without where clauses

The gist of the proposal is to allow the following syntax as a convenience to using a where clause with a same-type constraint. There's not really much else to say here.

This might be invalid though since I discovered it is mentioned in the Generics Manifesto as a special case of generic extensions. Is it implemented yet? Maybe we can ship this with 4.2 as a starting point?

class Foo<T> {...}

extension Foo<Int> {...}

// Current option
extension Foo where T == Int {...}
8 Likes

Good suggestion.

I frequently find myself naturally writing out extension Foo<Int> {...} and then being corrected by the resulting compiler error. It would be nice if either syntax worked.

2 Likes

I like the idea. @anthonylatsis you should write a formal proposal for this. This is indeed mentioned in the manifesto. Even though it's below 'Parameterized extensions' section, the syntax itself does not depend on that particular feature. It's just an alternative syntax for Concrete same-type requirements.

However we could discuss if we should generalize the syntax not only here but in other places of the language as well (only if there are other places where it might fit)!?

the syntax itself does not depend on that particular feature

Yes, that's true. I will, but I would like to first ask whether someone is already working on this. @Slava_Pestov @huon do you have any information on this?

1 Like

There is the potential for confusion between referring to an existing type and declaring a new generic parameter (people already accidentally shadow generic parameters of a type with generic parameters of methods all the time) so care has to be taken here. And while this doesn't depend on parameterised extensions being implemented, if that is a likely feature then the syntax for the two will need to interact in an understandable way. Taking one possible example syntax for parameterised extensions from the manifesto:

extension<T> Array { … }
extension Array<U> { … }
extension<T> Array<U> { … }

could be confusing if T and U are a combination of a new generic parameter and an existing type. Since the proposed syntax is shorthand for something that is already possible, it might be better to wait for a more complete picture before implementing it.

I don't see anything confusing enough to be concerned about – the first one is a generic parameter on the extension, the second one is a concrete subtype of a generic type. For me, it is as 'confusing' as class Derived<T>: Base<Int> {...} might seem to someone.

1 Like

I have a concern. When the generic argument is a class, you can currently have two distinct extensions:

// Base and its subclasses
extension Foo where T: Base { ... }

// Base only
extension Foo where T == Base { ... }

What would extension Foo<Base> mean? What would be the syntax for the other one, then?

Sure, that's a valid opinion. if we knew concretely what the syntax for all planned generics features would be then there could be a discussion about if combinations were confusing or not. But that was just the example from the manifesto, and my wider point was that we don't know what the syntax or supported features will be, so care has to be taken, particularly for shorthand syntax that doesn't add new capabilities.

What would extension Foo<Base> mean?

Of course, T == Base. The syntax for the other one is the usual generic way: T where T is a subclass of
Base.
@gwendal.roue These are established and very natural rules. In the case of functions,

func foo<T>(_ arg: Foo<T>) where T == Base
func foo<T>(_ arg: Foo<T>) where T: Base

The first one is, in contrast to the second, I hope understadably, equivalent to func foo(_ arg: Foo<Base>)

1 Like

That "of course" is quite quick!

But I least I understand that the convenience syntax is not supposed to replace the current one: there will be a way out of the default.

My though is that if swift is ever going to support protocols as concrete types (allowing for example Array<SomeProtocol> to be a valid type) then we need to use both the where T == Base and where T: Base depending on the context.

If it is a "non-inheritable" type such as a struct or enum, it uses the where T == Base syntax because that is what it has to be. When you use a protocol or class it uses where T: Base. Otherwise you end up where anything that has "inherited" a protocol can use the extension while subclasses can't which would be inconsistent.

Sorry, I am really tired and my brain isn't working and for some reason I was thinking that where T == Base worked only with Base not subclasses of Base.

Can you explain what is meant by "we need to use both depending on the context"?
I'm not sure if this brings clarity, but a same-type requirement and a subtype requirement are different things that have never had any problems in coexisting in the language. A generic entity that has a same-type requirement can't be used on anything other than that very type, while a subtype requirement makes it possible to use the entity on any subclass and the class itself. When it comes to whether you can cast generic types (i.e. Foo<Derived> as Foo<Base>) to be able to use some functionality that is under a same-type constraint, it is more about variance of generic types than syntax or types of constraints.

I don‘t understand the issue being raised here. There is literly no question about whether it should be : or ==. The manifesto explicitly mentions the answer to that already:

Note that when one is extending nominal types, we could simplify the syntax somewhat to make the same-type constraint implicit in the syntax [...]

The new syntax is not meant to replace anything, it‘s a convenience alternative, if you don‘t like it you don‘t have to use it and can stick to the where clause which still allows you to express : when required.

While I can fully appreciate why the proposed syntax must mean == and not :, the fact is that based on the reactions here from people very experienced in Swift, there is a question as to its meaning. This suggests that, should this idea be implemented, other users will have the same confusion.

It's not a matter of "if you don't like it you don't have to use it" because code is read more often than it is written. If even experienced users can misinterpret this shorthand, then we must give consideration as to whether its inclusion in the language will do more harm than good, especially here where misinterpreting == to mean : can lead to subtle errors.

1 Like

I've edited my message because I'm really tired and my brain wasn't working 100% when I wrote that post. Sorry for adding confusion.

While you are right in what you said, I still don‘t understand how experienced and even new swift developers can misinterpret the same-type-constraint. I‘d really appreaciate if you can help me to understand it.

// this seems already trivial, no?
_ = Array<Int>()

// what is the isse with this shortcut then?
extension Array<Int> { ... }

Int has no subtypes (yet), so that's obviously not an issue in your example. However, consider the "obvious" (and wrong) way of writing an extension for an array of things that conform to P:

protocol P { }
extension Array<P> { /* ... */ }

T == Base literally means that T has to be exactly Base. Whether you can cast Foo<Derived> to Foo<Base> explicitly or implicitly to use some functionality depends on the variance of the generic type.

To illustrate:

class A {}
class B: A {}

class Foo<T> {}
extension Foo where T == A {
  func foo() {}
}

Foo<B>().foo() //  error: 'Foo<B>' is not a subtype of 'Foo<A>'

// because Foo isn't covariant

Well the mental model today is that protocols do not conform to themselfs which we keep repeating over and over again. It‘s not clear if that behavior will ever change but I see what you meant there. Furthermore the whole example that uses an Array to showcase the confusion is a little bit wanky since there is collection related covariance involved which for some reason creates a collision with this syntax. (IIRC there was some talk about explicitly marking generic type to make them covariant, maybe we need to settle down that topic first?)