[Pitch] Extensions on bound generic types

Hello, Swift Evolution!

I've been working on a pitch for enabling extensions of bound generic types using angle-bracket syntax, e.g. extension Array<String> {}. This feature was subsetted out of the original pitch for SE-0346. I've copied the current proposal draft below.

Please let me know your questions, thoughts, and other constructive feedback!

-Holly


[Pitch] Extensions on bound generic types

  • Proposal: SE-NNNN
  • Authors: Holly Borla
  • Review Manager: TBD
  • Status: Awaiting implementation
  • Implementation: apple/swift#41172, gated behind the frontend flag -enable-experimental-bound-generic-extensions

Introduction

Specifying the type arguments to a generic type in Swift is almost always written in angle brackets, such as Array<String>. Extensions are a notable exception, and if you attempt to extend Array<String>, the compiler reports the following error message:

extension Array<String> { ... } // error: Constrained extension must be declared on the unspecialized generic type 'Array' with constraints specified by a 'where' clause

As the error message suggests, this extension must instead be written using a where clause:

extension Array where Element == String { ... }

This proposal removes this limitation on extensions, allowing you to write bound generic extensions the same way you write bound generic types everywhere else in the language.

Motivation

Nearly everywhere in the language, you write bound generic types using angle brackets after the generic type name. For example, you can write a typealias to an array of strings using angle brackets, and extend that type using the typealias:

typealias StringArray = Array<String>

extension StringArray { ... }

With SE-0346, we can also declare a primary associated type, and bind it in an extension using angle-brackets:

protocol Collection<Element> {
  associatedtype Element
}

extension Collection<String> { ... }

Not allowing this syntax directly on generic type extensions is clearly an artificial limitation, and even the error message produced by the compiler suggests that the compiler understood what the programmer was trying to do:

extension Array<String> { ... } // error: Constrained extension must be declared on the unspecialized generic type 'Array' with constraints specified by a 'where' clause

This limitation is confusing, because programmers don’t understand why they can write Array<String> everywhere except to extend Array<String>, as evidenced by the numerous questions about this limitation here on the forums, such as this thread.

Proposed solution

I propose to allow extending bound generic types using angle-brackets for binding type arguments, or using sugared types such as [String] and Int?.

The following declarations all express an extension over the same type:

extension Array where Element == String { ... }

extension Array<String> { ... }

extension [String] { ... }

Detailed design

A generic type name in an extension can be followed by a comma-separated type argument list in angle brackets. The type argument list binds the type parameters of the generic type to each of the specified type arguments. This is equivalent to writing same-type requirements in a where clause. For example:

struct GenericType<T1, T2> { ... }

extension GenericType<Arg1, Arg2> { ... }

is equivalent to

extension GenericType where T1 == Arg1, T2 == Arg2 { ... }

The types specified in the type argument list must be concrete types. For example, you cannot extend a generic type with placeholders as type arguments:

extension Array<_> {} // error: Cannot extend a type that contains placeholders

Similarly, the type parameters of the generic type cannot appear in the type argument list:

extension Array<Element> {} // error: Cannot find type 'Element' in scope

If a generic type has a sugared spelling, the sugared type can also be used to extend the generic type:

extension [String] { ... } // Extends Array<String>

extension String? { ... } // Extends Optional<String>

Source compatibility

This change has no impact on source compatibility.

Effect on ABI stability

This is a syntactic sugar change with no impact on ABI.

Effect on API resilience

This change has no impact on API resilience. Changing an existing bound generic extension using a where clause to the sugared syntax and vice versa is a resilient change.

Future directions

Parameterized extensions

This proposal does not provide parameterized extensions, but a separate proposal could build upon this proposal to allow extending a generic type with more sophisticated constraints on the type parameters:

extension <Wrapped> Array<Optional<Wrapped>> { ... }

extension <Wrapped> [Wrapped?] { ... }

Parameterized extensions could also allow using the shorthand some syntax to write generic extensions where a type parameter has a conformance requirement:

extension Array<some Equatable> { ... }

extension [some Equatable] { ... }

Writing the type parameter list after the extension keyword applies more naturally to extensions over structural types. With this syntax, an extension over all two-element tuples could be spelled

extension <T, U> (T, U) { ... }

This syntax also generalizes to variadic type parameters, e.g. to extend all tuple types to provide a protocol conformance:

extension <T...> (T...): Hashable { ... }

Note that SE-0346 and this proposal solidify using the extension <T> syntax for parameterized extensions, because this proposal specifies that a type in angle brackets after a generic type name in an extension is an application of type arguments, not a declaration of new type parameters.

69 Likes

Out of curiosity, was there a particular reason or controversy that separate this from the original proposal? Is there some aspect of the proposal we're supposed to focus on?

2 Likes

No, this was the uncontroversial piece of SE-0346, so we decided to subset it out so that it didn't get lost under the other parts of the original proposal. It also made sense to consider bound generic extensions separately, because IMO even if SE-0346 had not been accepted, this piece still makes sense for the language.

17 Likes

This seems like a no-brainer. The future directions stuff at the end is dicier, but the main proposal strikes me as a good idea with no discernible downsides.

It seems like in general we are moving toward a world where angle braces are equivalent to — maybe eventually even just sugar for? — more verbose type specifications that use constraints, i.e. for every X<Y> there exists an X where Y equivalent. I’m not sure if it’s possible to achieve that equivalence in all contexts, but it does seem like a good direction for the language to move.

18 Likes

Yes please! +1

1 Like

This looks pretty solid in my book. I have one question however: what about types that have more than one generic parameter but you want to make an extension on just one of those. For example: Result - say I want to make an extension where the Failure is Never. Would the _ syntax being forbidden also be applied to prevent extension Result<_, Never> { ... }?

4 Likes

+1 for me, this flows naturally and follows the direction already set in motion by the previous proposal so this seems the very natural next step!

Any reason why placeholders would not be allowed, and would be equivalent to the corresponding where clause not being included in the equivalent form?

Is it because we want to take it step by step and keep this one simple for now (and if so, support for placeholders could probably be added in "Future Directions")? Or are there complications or constraints I'm missing?

For example if we allowed it I'd imagine the following:

struct GenericType<T1, T2> { … }

extension GenericType<Int, _> // extension GenericType where T1 == Int

extension GenericType<_, String> // extension GenericType where T2 == String

extension GenericType<_, _> // allowed but generates a warning: list with all parameters being placeholders is unnecessary, fix-it: delete <_, _>
2 Likes

You need parameterized extensions to make the <> syntax work.

_ doesn't mean "unconstrained", it means "infer a concrete type for me" - it's the same reason why you can't use _ in an opaque return type, e.g. some Result<_, Never>. To extend a generic type and constrain only a subset of them, you'd need something like

extension <T> Result<T, Never> {}

or

extension Result<some Any, Never> {}

This is also how you would constrain only one type parameter of Result when using it as a function argument, e.g.

func test<T>(result: Result<T, Never>) { ... }
20 Likes

Ah, good point, missed that subtlety, thanks! :+1:

3 Likes

Neither of those are as simple/intuitive as I guess I would have hoped. I understand the reasoning, but it just seems a bit unfortunate.

Thanks for the clarification.

2 Likes

I agree; I would love a nicer way to spell an unconstrained type parameter, e.g. some Any, for opaque types and for this. I think this should be explored as a separate proposal, though, because it's applicable to a bunch of different places where you have to write some Any or an unconstrained <T>.

6 Likes

Can we make omitting the parameter work? For example:

extension Result < , Never>
extension Result <String, >

// I would require white space (it can invoke a fix-it):
extension Result <, Never> // Rejected, suggesting add a space
extension Result <String,> // Same as above

// We can also accept and warn redundancy of these:
extension Result < , > // Warn, suggest dropping the whole `< , >`
extension Array < > // Same as above

My suggestion is to keep this pitch focused on what's proposed, and move discussion of syntax for a different proposal to a new thread.

14 Likes

+1. This indeed is a no-brainer.

3 Likes

+1 This really should just work.

1 Like

I agree that this is a good analogy for programmers to draw, because it provides a stepping stone from the familiar <> syntax to the more expressive where clause version. I'd also love to implement SourceKit refactoring actions between the two in cases where it's possible! Even in the cases where you cannot write a where equivalent (e.g. local variables), I think it's a helpful way of thinking about bound generic types.

10 Likes

Yes, “stepping stone” is a nice grounding concept here. There are all these distinctions that matter tremendously in the realms of formal type theory and implementation, but just feel like artificial barriers from the POV of a language user. I’m all for building these syntactic bridges!

I wonder if one day we will get there? Would this ever make sense, in some hypothetical future?

let a: some Sequence<String>
let b: some Array<String>     // “some,” but in fact there’s only one possible type
let c: Array<String>          // equivalent to previous

Hmm, this probably becomes nonsense if the language gets to determine that some doesn’t really mean some in some circumstances. Still, I like the notion of having a kind of unified surface for these very different type system features.

For most use cases, this is a win.

It causes new confusion for generic type aliases though.

For example, given

typealias Get<Success> = Result<Success, any Error>

Get<Int> is Result<Int, any Error>.

However, Get becomes Result in an extension, losing its Error constraint.

extension Get where Success == Void {
  var property: Void { () }
}

I.e. both of these property calls compile.

Get { }.property

enum 👎: Error { }
Result<_, 👎>.success(()).property

So either

extension Get<Void> { }
  1. will not be allowed to compile.
  2. Get<Void> in that context will be different than it is when not preceded by the extension keyword.
  3. The rules for generic type alias extensions have to change.

1 & 2 lead to new inconsistencies.
The current rules are wrong, so I'd like to see 3 happen, but are you even allowed to fix it, or is it a compounding mistake we have to live with forever?

4 Likes

Thank you for pointing this out; I did not know about this oddity with extensions of generic type aliases. I agree that the current behavior seems wrong, and indeed the current implementation of this proposal will preserve the Failure == any Error requirement of the generic typealias inside of an extension Get<Void> { ... }:

typealias Get<Success> = Result<Success, any Error>

extension Get<Void> {
  var property: Void { () }
}

func test(result: Result<Void, any Error>) {
  result.property // okay
}

enum MyError: Error { }
func test(result: Result<Void, MyError>) {
  result.property // error: Property 'property' requires the types 'MyError' and 'any Error' be equivalent
}

The solution I'm inclined to advocate for (unless there is a justification for this behavior that I'm not thinking of) is to start warning about this code in Swift 5.x mode, possibly with a fix-it to extend the underlying type directly, and unify the behavior in Swift 6.

Thoughts?

19 Likes

Agreed! Unless there’s something I’m missing, the current meaning of that extension Get<Void> seems straightforwardly misleading, regardless of the separate behavior you’re pitching for normal generic types.

As for the pitched behavior, wholehearted +1.

5 Likes