Simpler syntax for exact-type extensions of generic types

I was fiddling around with some ideas earlier, and came across this scenario:

enum Placement {
    case before
    case inBetween
    case after
}
typealias PlacementSet = Set<Placement>

extension Set where Element == Placement {
    static var surround: Set<Placement> = [.before, .inBetween, .after]
}

As I was writing this, I was struck by the verbosity of the Set extension. I tried changing it to:

extension Set<Placement> {

and

extension PlacementSet {

... but got a compiler error about how "Constrained extension must be declared on the unspecialized generic type 'Set' with constraints specified by a 'where' clause".

Can we add support for these sort of "exact type" extensions? The requirement of the where clause obfuscates the purpose of the extension, and it makes extending typealiased things impossible.

14 Likes

Just for reference, this is listed at the bottom of parameterized extensions in the generics manifesto: swift/GenericsManifesto.md at main · apple/swift · GitHub

4 Likes

Parameterized extensions are a different feature. They are extensions that themselves take a generic parameter. The pitch here is just for syntactic sugar for declaring constrained extensions.

1 Like

Yes, but that section does mention this case:

When we're working with concrete types, we can use that syntax to improve the extension of concrete versions of generic types (per "Concrete same-type requirements", above), e.g.,

extension Array<String> {
  func makeSentence() -> String {
    // uppercase first string, concatenate with spaces, add a period, whatever
  }
}

Ahh, I missed that. In any case, this syntax could be introduced without actually introducing generic extensions.

3 Likes

The previous pitch: Allow static extensions of generic types to be written without where clauses

The main concern was about people potentially misinterpreting the shortcut as : Type rather than == Type.

I honestly think this is a non-issue, anywhere else in Swift Set<P> means Set where Element == P.

1 Like

There is no need to speculate: it’s not simply a potential issue. I too didn’t think much of it but actual, real-life people on this forum in fact had this misinterpretation. One cannot merely dismiss such empiric evidence.

Perhaps its best to ask those people to describe their train of thought back then. Why would someone think of, say, extension Foo<Class> as a shortcut for extension Foo where T: Class knowing that generic types are not covariant in Swift and Foo<Class> is equivalent to Foo where T == Class anywhere in Swift (covariance exceptions aside)?

@gwendal.roue, would you mind sharing some thoughts?

Hello, sure! :-)

A possible way to solve the : / == question is to make it moot by picking up a higher-level rule.

For example, one that is super easy to understand and explain:

If extension WHATEVER { ... } can be written, then this extension extends all values of type WHATEVER, period.

Applied to our particular case:

extension Array<MyClass> extends all values of type Array<MyClass>. It happens, on the way, that it is equivalent to extension Array where Element == MyClass. But this == is a consequence of the rule.

Sorry if I wasn't clear enough, I was actually asking about why you hesitated between == and : or the reasons the proposed syntax would confuse you :)

It is already a rule that a bound generic type is equivalent to the unbound supertype with same-type constraints on its type parameters.

This is currently true as well, but it doesn't mention that the extension applies to subclasses too when WHATEVER is a class.

In theory, the confusion could be avoided (or at least mitigated) by allowing:

extension Array<Int> {...} // sugar for `extension Array where Element == Int {...}`
extension Array<T: BinaryInteger> {...} // sugar for `extension Array where Element : BinaryInteger {...}`

and mentioning both uses together in the docs, such that it'd be hard to notice one without also seeing the other.

The key phrase there is probably "in theory", though.

3 Likes

So this exact scenario just came up with the recent SIMD changes on the swift-5.0-branch (swift-evolution/0229-simd.md at master · apple/swift-evolution · GitHub). The various types such as float2 were formerly concrete types and are now type aliases for SIMD2<Float>, and so on. This means that the following no longer works:

extension float2 {...}

I'm thinking we should allow extensions of bound generic types, at least those hidden behind type aliases. @xwu does your objection still stand if this is permitted for type aliases only?

10 Likes

And that misinterpretation has been corrected.

People could potentially misinterpret *anything*. Those people can then learn what the actual interpretation is, if they need it. The Swift syntax for generics, eg. “Array<Foo>”, is well-defined and has an existing meaning.

There is no reason whatsoever to let fear of misinterpretation prevent us from making reasonable progress. The correct response to misinterpretation is education.

2 Likes

I noticed that this compiles:

typealias StrKeyFpValueDict<V> = Dictionary<String, V> where V: FloatingPoint

extension StrKeyFpValueDict {
    func willBeAvailableForAnyDictNotJustThoseWithStringKeyAndFpValue() {
        // ...
    }
}

Should it?

3 Likes

No, that's a bug. We need to diagnose extensions of type aliases that don't map the generic arguments 1-1.

Type aliases that add new requirements are OK though, and should be handled correctly. This is how we replaced CountableRange with a typealias in Swift 4.2.

4 Likes

If a particular unintended interpretation is relatively frequent even among experienced users, it's a sign that the design is counterintuitive in some way. The correct response is to look for ways to improve the usability of the design, not to "educate" users on why they're wrong.

I strongly doubt that any substantive changes to the existing meaning of Swift generic syntax will take place now that we are in the era of API stability. Regardless of whether it was the right decision to make Array<Foo> behave as it does when Foo is a protocol, that is the decision Swift has made and it is extremely unlikely to change.

Perhaps some new syntax could be introduced to handle the “conforms to protocol” case, such as Array<Element: Foo>, but that is beyond the scope of the discussion at hand and it still would not change the meaning of Array<Foo>.

Given that there is a type currently spelled Array<Foo>, and given that the spelling for that type is not going to change, it follows that the obvious way to write an extension of that type is extension Array<Foo>.

This is the straightforward composition of two orthogonal features: the existing spelling for a generic type, and the existing spelling for extending a type. There is currently a limitation preventing them from being used in the natural way together. When we lift that limitation, the meaning of the resulting code follows directly from the existing features.

3 Likes

Sure, I completely agree that it is very straightforward, and yet people have interpreted this spelling in unintended ways. Therefore, the current limitation is preventing a point of confusion and we need to consider carefully how to address that confusion in designing how to lift the limitation. It does not follow that all compositions of two existing features ought to exist.

There's always going to be people who misinterpret things, no matter how obvious they are. It's not worth designing for those misunderstandings unless they're commonly held. I don't believe this one is.

2 Likes