Okay, I know the title is a little confusing, bear with me here.
You can write a generic constraint with either an equality or a subtype relation:
extension Collection where Element == Int {}
extension Collection where Element: BinaryInteger {}
But you cannot, today, write a constraint that specifies a particular generic type, leaving its generic parameters unspecified.
For example, suppose you want to extend “collections of optionals”. You cannot write:
extension Collection where Element: Optional {}
// error: reference to generic type 'Optional' requires arguments in <...>
And the same error occurs if we replace the “:
” with “==
”.
• • •
First of all, why would one even want to do this?
Here’s a simple example:
Dictionary
has a method called compactMapValues
, which makes a new dictionary by transforming each value, and dropping the entries that got mapped to nil. That works for all dictionaries, and it requires passing in a transformation closure.
But suppose we have a dictionary of optionals (perhaps loaded from disk or fetched from a server) and we find ourselves calling dict.compactMapValues{ $0 }
to remove the nils and unwrap the values.
We don’t actually transform the values, so we might decide to make a little convenience method that lets us simply write dict.compacted()
instead:
func compacted() -> [Key: Value.Wrapped] {
return self.compactMapValues{ $0 }
}
This is nice and clean, and puts things at the proper level of abstraction for us. The only problem is, where does that function go?
We want to put it in a constrained extension of Dictionary
like so:
extension Dictionary where Value: Optional {
func compacted() -> [Key: Value.Wrapped] {
return self.compactMapValues{ $0 }
}
}
But as mentioned above, the constraint Value: Optional
results in a compile-time error.
• • •
There is a workaround, but it’s pure boilerplate which involves a new protocol that only one type conforms to, and pollutes the namespace of Optional
with a superfluous property that just returns self
:
protocol OptionalProtocol {
associatedtype Wrapped
var optionalValue: Wrapped? { get }
}
extension Optional: OptionalProtocol {
var optionalValue: Self { self }
}
With that in place, we can write:
extension Dictionary where Value: OptionalProtocol {
func compacted() -> [Key: Value.Wrapped] {
return self.compactMapValues{ $0.optionalValue }
}
}
And finally our method is available for use as dict.compacted()
.
• • •
If we wanted to do something similar for another generic type—say an extension on “collections of dictionaries”—then we’d need another boilerplate protocol and another self
-returning property.
The workaround works, but it doesn’t scale well, and it isn’t necessarily obvious. So I propose that we allow generic types to be used as constraints directly, without any ceremony or boilerplate. This should just work:
extension Dictionary where Value: Optional {}