Skip to content

Instantly share code, notes, and snippets.

@Azoy
Last active June 11, 2019 01:28
Show Gist options
  • Save Azoy/eb6877650c7ab0214fb946c592531a3e to your computer and use it in GitHub Desktop.
Save Azoy/eb6877650c7ab0214fb946c592531a3e to your computer and use it in GitHub Desktop.
Parameterized Extensions

Parameterized Extensions

Introduction

This proposal aims to enable users to be able to supply extensions with generic parameters and to be able to extend specialized types.

Swift-evolution thread: Parameterized Extensions

Motivation

Currently in Swift, one cannot give extensions generic parameters to build expressive generic signatures for new members. Consider the example:

// You can't really express this extension where any array whose element is an optional type.
// This doesn't work.
extension Array where Element is Optional {
  var someValues: [T] {
    var result = [T]()
    for opt in self {
      if let value = opt { result.append(value) }
    }
   return result
  }
}

The above extension is almost impossible to currently express. You could get around it in a few ways by:

  1. Creating an OptionalProtocol and giving conformance to Optional:
protocol OptionalProtocol {}

extension Optional: OptionalProtocol {}

extension Array where Element: OptionalProtocol {}
  1. Using a function instead of a computed property and making the function generic:
extension Array {
  func someValues<T>() -> [T] where Element == T? {
    // ...
  }
}

Both workarounds are sub-optimal. With #1, you have to go through a level of indirection to ensure that the element type is Optional, but you aren't granted any of the methods on Optional for free. #2 is better, but what you really wanted was a computed property which is a constraint on the expressivity that Swift aims to provide with its Generics model. This also starts to become boilerplate when you want to have multiple members with the same generic signature. It would be nice to be able to define a common generic signature for all members, including computed properties.

Extending specialized types is also an awkward example of the current generics model for extensions.

struct Pair<Element> {
  let first: Element
  let second: Element
}

// error: constrained extension must be declared on the unspecialized generic
//        type 'Pair' with constraints specified by a 'where' clause
extension Pair<Int> {
  var sum: Int { first + second }
}

// Okay, but why not the more straight forward syntax?
extension Pair where Element == Int {
  // ...
}

Requiring users to use the second syntax is a little weird because now they have to remember the generic parameter names, and some types may not provide meaningful parameter names (extension SomeType where T == U, what is T and what is U?).

Proposed solution

Parameterized Extensions

Extensions can now be decorated with a generic parameter list to be used with constructing a generic signature when extending types. Using the array of optionals example above, we can now write:

extension<T> Array where Element == Optional<T> {
 // ...
}

to extend all arrays whose element type is an optional of any type. You can of course use the optional type sugar now to do:

extension<T> Array where Element == T? {}

With a generic parameter list, users can also define generic types that conform to protocols.

// Extend all arrays whose elements are optionals whose wrapped type conforms to FixedWidthInteger
extension<T: FixedWidthInteger> Array where Element == T? {
  var sum: T {
    // for all non nil elements, add em up
  }
}

Extending types with same type requirements

Throughout the language grammar, supplementing a generic type with types produces a generic signature with something called same type requirements. We saw them earlier with Array where Element == T? where the generic parameter Element has the same type as T?. We can simplify this syntax into what we're all comfortable writing, Array<T?>.

// Extend array whose element type is an optional T
extension<T> Array<T?> {}

Extending specialized types

This feature goes hand in hand with Extending types with same type requirements, but I propose that we finally allow extending specialized generic types (also known as concrete types). As mentioned in Motivation, we can now use this simpler syntax in extensions without worrying about what the extended type's generic parameter is named.

// We don't need to know that Array's only generic parameter is named Element
// This is especially useful for types that have not so great generic parameter
// names, such as T, U, V, etc.
extension Array<String> {}

Extending sugar types

With the above three new features, we can extend sugar types now. Whether it be generic or a concrete type, users can opt into a single mental model when working with types like [T], [T: U], and T? instead of switching between their canonical form when extending these types.

Examples:

extension [String] {
  func makeSentence() {
    // ...
  }
}

// Extend Array where the element type is an optional T
extension<T> [T?] {}

Detailed design

Swift's extension grammar changes ever so slightly to include a generic parameter clause after the extension keyword:

extension-declaration: attributes (opt) access-level-modifier (opt) extension generic-parameter-clause (opt)
                       type-identifier type-heritance-clause (opt) generic-where-clause (opt) extension-body

It's important to note that the extensions themselves are not technically generic, rather what's happening is that we're supplementing the extended type with new generic parameters. What this allows us to do is essentially copy this new generic signature for all new members within the extension rather than writing out a new generic function for each and every member. It also allows for those generic computed properties that I discussed earlier in Motivation.

Banning generic parameters extensions

When one extends the generic parameter that was declared in the extension, the compiler will diagnose that it's currently unable to do so.

extension<T> T {} // error: cannot extend non-nominal and non-structural type 'T'

I discuss more about this in Future Directions

Conditional Conformance

Parameterized extensions allow for some very neat generic signatures, including conditional conformance.

// If Array's Element type is Equatable, conform to Equatable
extension<T: Equatable> [T]: Equatable {}

Source compatibility

This change is additive, thus source compatibility is unchanged. All of the features discussed currently don't compile, so we aren't hurting source compatibility.

Effect on ABI stability

This feature does affect the ABI, but it doesn't break it. There are cases where one could accidently break their ABI. For example, moving the generic signature from an extended function to the extension is ABI breaking.

// Before
extension Array {
  func someValues<T>() -> [T] where Element == T? { /* ... */ }
}

// After (ABI broke)
extension<T> Array where Element == T? {
  func someValues() -> [T] { /* ... */ }
}

Another example would be using this new syntax for conditional conformance. Rewriting your conditional conformance to use parameterized extensions instead would break ABI.

// Before
extension Array: Equatable where Element: Equatable {}

// After (ABI broke)
extension<T: Equatable> [T]: Equatable {}

For simple cases like renaming extensions to use same type constraints or using the sugar types is ABI compatible.

// Before
extension Array where Element == Int {}

// After (ABI not broke)
extension [Int] {}

Effect on API resilience

This feature does not expose any new public API.

Alternatives considered

There were a couple of minor alternatives that I considered, one being to disallow sugar types. While it could make sense, many of us write properties and parameters using this syntax, so it makes sense to be able to extend them as well to be consistent.

Future Directions

Right now, extending generic parameters are banned. One could extend a generic parameter to add members to all types in the future.

// Extend every type to include a instance member named `abc`
extension<T> T {
  func abc() {}
}

let x = 3
x.abc() // ok

// Extend every type that conforms to ProtoA to conditionally conform to ProtoB as well.
extension<T: ProtoA> T: ProtoB {}

Parameterized extensions could work really well with new generic features such as extending non-nominal types and variadic generics. Using the infamous example from the Generics Manifesto:

// Extend tuple to conform to Equatable where all of its Elements conform to Equatable
extension<...Elements: Equatable> (Elements...): Equtable {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment