Parameterized Extensions

to be upfront, i’m just worried about the possibility of some really hard to parse (as a human) declarations like

DeFrenZ explained the issue with your suggestion, but that doesn't answer this concern. The answer to this concern is that you can write out the constraints later, so

// Discouraged
extension <T, U: LongProtocolName1, V: LongProtocolName2> SomeObject<T, U, V> {}
// Better
extension<T, U, V> SomeObject<T, U, V> where
  U : LongProtocolName1, V : LongProtocolName2 {}

// Discouraged
extension <T: Foo & Bar & Baz, U: Foo & Bar & Qux> SomeObject<T, U> {}
// Better
extension<T, U> SomeObject<T, U> where
  T : Foo & Bar & Baz,
  U : Foo & Bar & Qux {}

I might've messed up the punctuation, but you get the idea.

It's the same problem as with functions with many generic parameters.

5 Likes

thanks for the reply

i guess not declaring the generic parts beforehand also slows down parsing and other checking...

one other idea though, what if you had to declare the generic parts just like a function on the object you extend, but can only constrain it using where afterwards?

like

extension SomeObject<T,U,V> where
  T: Decodable & SomeOtherProtocol
  U: Optional
  U.Wrapped == T
  V: Result<Any, Error>

in other words, require the declaration of the generic parts firstly (just like with a function), but instead of writing it potentially twice, just do it once and require the contraints to use where at the end

is that possible or does it still not meet the requirement as outlined by @DeFrenZ ?

In theory, perhaps. The problem with this approach, however, is that we're extending a type Array with element type T, but we haven't declared T anywhere in source. It'd be confusing to expect the compiler to synthesize generic types out of the blue like this. For this type of extension it's best to just use what's possible now with:

extension Array {}

where there's no confusion about what's happening.

Another aspect I see is that for every other context in Swift (structs, classes, enums, functions, subscripts, etc.) they have to explicitly declare new generic types that they're introducing:

// ok make generic type T for this struct
struct GenericStruct<T> {}

// ok make generic type T for this class
class GenericClass<T> {}

...

Again, it'd be really confusing to learn that extensions are the exception. It seems naturally that we'd need to explicitly declare the generic types for an extension too.

This type of extension is already possible in Swift.

// assuming SomeObject's generic types are named U and V
extension SomeObject where U: Codable, V: Codable {}

The parameterized extension version says that we're declaring three new generic types, T, U, and V. We're then extending a type SomeObject whose generic types align with T, U, and V.

I completely agree. When I read through this, it felt like your #4 was an extension on #3 (implementation wise). We can start with #3, evaluate if this meets most use cases or if we're satisfied with just this amount of rewrite, or extend it to support #4. (When I say extend support, I mean before we ship this feature, perhaps toolchains so others can learn the rewrite rules, etc.) I think it makes sense to start with #3, and see community input to check if #3 is enough for understanding, or if they prefer more complicated rules around rewriting.

2 Likes

thanks for the thoughtful reply, its appreicated!

hope this feature can get in soon enough ^^

I worry about one thing with parameterized extension, it give yet another syntax for generic extensions:

extension Array where Element: Codable { }
extension<T> Array<T> where T: Codable { }
extension<T: Codable> Array<T> { }

Shouldn't we allow the generic parameter to only be used with the where clause? This would reduce number of possible syntaxes and make code more consistent.

1 Like

Is this really a problem though? I think it‘s a implication of the general feature and the differences in syntax forms will be just personal preferences. It doesn‘t hurt you if your form is supported but it will hurt someone who prefers the other form if it‘s artificially unavailable.

4 Likes

I'd be in favour of consistency so that code is uniform and beginners don't have to ask themselves what's the difference between the two syntaxes.
I know I might be kind of alone in that thinking but wanted to say it anyway :slight_smile:

Apart from that... parameterized extensions would be really useful :slight_smile:

6 Likes

@Alejandro just respectfully and kindly checking if there is any progress with this feature? I doubt that it could land in 5.3 (not even with cherry-picking) even though I'd pay for it, but it would be cool if it could land on master soon-is.

4 Likes

I think I would too :smiley:

I'm working right now on an abstract data types library, and the lack of parametrized extensions is a major problem, that requires ugly workarounds.

1 Like

In my code I just want to re-build existing code, delete a lot of boilerplate and no longer to be forced to create boilerplate code. I've been waiting for this feature to land for many many years and am really glad that it's finally moving forward.

1 Like

What about putting generic types in their own brackets like this: extension Dictionary<String, <T>>? The syntax does look a bit weird to me, but so does extension <T> ... and it’d solve the problem.

Hi there @Alejandro, awesome work!
I recently bumped in this problem with some source-generation work where this would have helped a lot and Joe pointed me at this thread :slight_smile:

Just wanted to voice a supportive "this is great!" and looking forward to it - hope you can find some time to finalize the work and get it through review :heart:

4 Likes

Hi David,

This idea, or something like it, seems very sensible to me.

I have always wondered whether Swift might have potential issues with variable capture when it comes to type variables. Because existing types are always able to be 'captured' when defining a new type, it seems eminently sensible (for both machines and for people) to be able to easily distinguish between a 'type' identifier and a 'type variable' identifier in any context whatsoever.

This problem seems to be manifesting itself in the context of this discussion, where types and type variables can both potentially be referenced in the same extension 'introduction'.

In principle, I like your idea but, like yourself, I have some misgivings about angle brackets being the ideal solution.

One drawback of using angle brackets as a solution is that of course they are a bit 'heavy' to read. I fear that any increase in their use (particularly in nested contexts) would not find particular favour with humankind in general.

Another possible drawback is that, from what I understand, angle brackets already mean something quite specific in the context of generics, in that they serve to 'bind' type variables to a type definition, in a similar way to which ordinary brackets are used to introduce ordinary 'value' variables into function definitions.

In this context, then, using angle brackets to identify a type variable would look the same as the outer brackets, but mean something different. For this reason, it might not be suitable.

But the general idea has some appeal, for the reasons that @DeFrenZ has identified. If I may, I would like to re-articulate these issues as follows:

  1. In the context of extensions, a generic type already has its own 'variable names' for its component types. One must solve the problem of how one may or must refer to these type variable names in the extension.

  2. Perhaps even more problematic, in the context of constraining an extension, one may be attempting to constrain a generic extension to a specific concrete component type, and this is where the potential conflict between type identifiers and type variable identifiers emerges.

I have often wondered whether Swift could have a simple disambiguator between type identifiers and type variable identifiers — perhaps something like:

U     // A type identifier
'U    // A type variable identifier

With such a facility, your solution might look something like this:

extension Dictionary<String, 'T>

And all the machines and people would know what's going on.

I'm afraid, though, that this bird might have flown when it comes to Swift, and it might not be possible any more to do such a thing.

Anyway, I wish you all the best.

3 Likes

Hi, I'm really sorry for this late reply, but I think it's worth discussing.

I read through this thread, and find that what you are proposing can be divided into two parts:

  1. Specialized-type extension: Enabling extension of generic type with type parameter like extension Array<Int> {}
  2. Generic parameterized extension: Enabling extension with generic type parameter like extension<T> Array where Element == Optional<T> {}

I'm supportive of 1. It is really readable and writable, and maybe has few problems around ABI.

About 2, I think there is another syntax to achieve it, so I want to suggest it as an alternative. Basically, it would make implementation easier keeping the core of the feature.

Generic parameterized extension works like this.

// Currently error
extension Array where Element is Optional {}
// With parameterized extension
extension <T> Array where Element == Optional<T> {}

However, there is another possible position, after where. I'd like to call it as 'generic where'. It works totally equal to extension <T>. Especially, T is usable inside extension.

// Having type parameter clause after `where`
extension Array where <T> Element == Optional<T> {}

As far as I checked examples in this thread, there are no situations that are impossible with generic where.

Merits

Consistency

It solves the concern raised here. Because it doesn't allow diverse declaration, codes would become more consistent.

// With parameterized extension
extension Array where Element: Codable { }
extension <T> Array<T> where T: Codable { }
extension <T: Codable> Array<T> { }

// With generic where, only this is allowed
extension Array where Element: Codable { }

And this is the biggest point. It rescues this. Generic where has less variation than parameterized extension. Therefore, we have less possibility to accidentally break ABI. I don't have knowledge about implementation, but I think it would make implementation easier.

Generality

Also, it can be used outside of the extension. We can use this feature anywhere in the code. This is more consistent than an extension-only feature.

// limit Element to Optional
struct OptionalArray<Element> where <T> Element == Optional<T> {}

Currently, there are small needs for generic where outside of extension, but if we introduced variadic generics referred in future direction, this feature would be more necessary outside extension. This is an instant syntax, but you can find how it can be useful.

struct Collections<...(T: Collection)> where <Element: Numeric> T.Element... == Element {}
Collections<Array<Int8>, Set<Int8>>              // OK
Collections<Array<Int8>, Array<Int8>, Set<Int8>> // OK
Collections<Array<Int8>, Set<Int16>>             // Error

Without generic where, we would have to write Collections using dummy type parameter like this.

struct Collections<...(T: Collection), Element: Numeric> where T.Element... == Element {}
CertainNumericTypeVector<Array<Int8>, Set<Int8>, Int8> 

Of course, now the signature of variadic generics is not fully decided. It can be unnecessary if we introduce a stronger feature like #allequal which is referred in the pitch in 2016.

Sufficiency

One more merit of this expression is, there is no necessity to explicitly ban extension <T> T {}, because you cannot write it from the beginning. It's sufficient for what we want to achieve.

Discussion

Redundancy

It cannot help some situations. For example, this code won't get easier to read.

// With parameterized extension
extension <T> Dictionary<String, T> {}
extension <T> Array<T?> {}

// With generic where (equal to current Swift)
extension Dictionary where Key == String {}
extension Array where<T> Element == T? {}

In addition, though generic where reduces variations, some of variations still remain like the next codes. I think we don't have to care it, because no one would make the code longer using this feature. Or, we can ban using == between (not variadic) associated type and bare T in where clause.

// With parameterized extension
extension Array where Element: Codable {}
extension <T: Codable> Array where Element == T {}

// With generic where
extension Array where Element: Codable {}
extension Array where <T: Codable> Element == T {}

Also, generic where has additional variation in function signature.

// With current Swift
func optionalArray<Element>(_ value: Array<Element?>){} 

// With generic where
func optionalArray<Element>(_ value: Array<Element?>){} 
func optionalArray<Element>(_ value: Array<Element>) where <T> Element == Optional<T> {} 

I think it is within the range of acceptable redundancy. At least, about ABI, we don't have to specially treat these two functions to be equal, since its type parameters are different. But I admit there is room for discussion.

Visibility of type parameter

As mentioned in the earlier discussion, I think the parameter should be private in extension, generic types, and function. It would be nice if you can explicitly publish it.

struct OptionalArray<Element> where <T> Element == Optional<T> {
    typealias Wrapped = T
}
OptionalArray<Int?>.Element // Int?
OptionalArray<Int?>.T       // Error
OptionalArray<Int?>.Wrapped // Int

Disturb future direction

It can seem a demerit that it can't enable extension <T> T {} in the future.

But it isn't serious. We don't even think of writing extension <T> T {} with current Swift. What we tend to try is extension Any {}, but after trying we find this is disallowed. So, it is more natural to lift limitations and enable extension Any {}.

We can say a similar thing for protocol conformance. What we naturally write with current Swift is extension ProtoA: ProtoB and not extension <T: ProtoA> T: ProtoB {}. It is more natural to enable extension ProtoA: ProtoB, isn't it?

Adding generic parameterized extension makes extension <T> T {} and extension <T: ProtoA> T: ProtoB {} natural, but we don't have to have two ways to achieve almost the same thing. We can simply lift some of the limitations and things referred in future direction can be achieved.

Therefore, it is not a problem that generic where can't enable extension <T> T {}. It is also really easy to reject enabling extension Any {} in the future discussion, because generic where doesn't imply even possibility to enable such a feature.

Conclusion

There are many merits to choose generic where:

  • We have no needs to explicitly ban the use of extension <T> T {}.
  • We can have less variety of spellings so that expressions would be consistent across developers.
  • where <T> can also be used anywhere, which will be more useful with variadic generics.

Some discussions about generic where:

  • In some cases, we can't reduce redundant codes, and have new variations of the function signature.
  • We must wait until extension Any {} and extension ProtoA: ProtoB {} are achieved, to get the feature we expected for the future parameterized extension.

I think it can be a good alternative to generic parameterized extension in that it can solve some concerns raised in this thread. How do you think?

4 Likes

Wow that was an interesting read. Generalizing it over where seems to enable even more generic features, which I‘m all in.

In the future we might want to allow generic associated types or even generic Self, so don't see why we shouldn't also allow a generic type to be generic:

// with generic where
struct OptionalArray<Element> where <T> Element == Optional<T>

// double (nested?) generic
struct OptionalArray<Element<T>> where Element == Optional<T>

@ensan-hcl I don't see why you should't be able to publish the generic type as it's already available to the user:

OptionalArray<Int?>.Element.Wrapped

// that means that your
typealias Wrapped = T

// equals
typealias Wrapped = Element.Wrapped
2 Likes

Maybe should? I thought sometimes it would have usages, but I don't have any concrete example. Perhaps it would have some usages with variadic generics. I don't think it's a must now.

struct Collections<...(T: Collection)> where <Element: Numeric> T.Element... == Element {
    // Maybe have some usage because getting one type from (T.Element...) is not always easy
    typealias Element = Element
}

Well I personally don‘t like the idea of shadowing a type like this. That just shows that the generic type from the where clause should probably be implicitly public from the start.

In the above example I see a few issues:

  • it‘s shadowing the type for all the nested types and members
  • typealias T = T is strange, probably even for the compiler itself

Edit: Here is the potentially somewhat related thread. It‘s 3 years old so I need to re-read it to recall why I wanted this feature, and how exactly is related.

1 Like

I see. I don't have any strong opinion about it. Then I feel implicitly publishing T in where clause of type declaration and hiding that in where clause of extension would be the most balanced.

(EDIT) Reading your thread, I now feel it should be private. If we allow the lookup of generic parameters, T in where clause of type declaration can be public.

+1