Parameterized Extensions
- Proposal: SE-NNNN
- Author: Alejandro Alonso
- Review Manager: TBD
- Status: Awaiting Review
- Implementation: apple/swift#25263
Introduction
Hello Evolution, this is my first draft at a proposal for Parameterized Extensions. I would greatly appreciate any feedback, suggestions, and examples to use as well! If there's any areas you feel could use some clarification, let me know! I know I'm just pitching this, but hopefully I laid out the structure of this well enough to start discussion on.
This proposal aims to enable users to be able to supply extensions with generic parameters and to be able to extend specialized types.
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:
- Creating an
OptionalProtocol
and giving conformance toOptional
:
protocol OptionalProtocol {}
extension Optional: OptionalProtocol {}
extension Array where Element: OptionalProtocol {}
- 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 {}