String
was extended in beta 6 to implement RangeReplaceableCollectionType
. This means that, via inheritance, it also implements ExtensibleCollectionType
.1
ExtensibleCollectionType
is interesting, because it requires the collection to support an empty initializer. This means that, without having to resort to shenanigans, you can write a generic function that takes an ExtensibleCollectionType
and returns a new one.
Since they were changed to return eagerly-evaluated results, the non-member filter
and map
have returned arrays, no matter what. This is a bit frustrating when working with some non-array types, such as String
:2
let vowels = "eaoiu" let isConsonant = { !contains(vowels, $0) } let s = "hello, i must be going" // filtered will be an array let filtered = filter(s, isConsonant) // and then we have to turn it back into a string let only_consonants = String(seq: filtered) // only_consonants is "hll, mst b gng"
It would be nice to have a version of filter
that took a String
and returned a String
instead of an Array
. 3 Even better, it would be nice to have a single generic version that worked on both arrays and strings.
Here’s one:
func my_filter <C: ExtensibleCollectionType> (source: C, includeElement: (C.Generator.Element)->Bool) -> C { // use the `init()` from `ExtensibleCollectionType` var result = C() for element in source { if(includeElement(element)) { // append is also part of `ExtensibleCollectionType` result.append(element) } } return result } // my_filter returns a String when passed one: let only_consonants = my_filter(s, isConsonant)
Since this is possible, should Swift’s filter
and map
be changed to be like this? Maybe, but I can think of a couple of reasons why not.
First, it’d be a bit inconsistent and possibly surprising. Not all collections are extensible collections. Dictionary
isn’t. Range
and StrideTo
even less so – they’re like “virtual” collections that don’t really have individual elements at all. So there’d still need to be versions that took these collections and returned an array. So when calling filter
, you’d need to know whether your collection was extensible to know whether you were going to get back the same collection type or an array.
There’s precedent for this kind of thing. lazy
gives you back different types depending on what you pass in. But lazy
is very explicit. map
and filter
would be a bit more subtle, and bear in mind subtle maybe-unexpected behaviour was probably the reason lazy evaluation was moved into the lazy
family in the first place.
Second, maybe you do want an array back. This can be catered for – declare a second version of my_filter
like so: 4
func my_filter <C1: ExtensibleCollectionType, C2: ExtensibleCollectionType where C1.Generator.Element == C2.Generator.Element> (source: C1, includeElement: (C1.Generator.Element)->Bool) -> C2 { var result = C2() for element in source { if(includeElement(element)) { result.append(element) } } return result } // same-type version will be used by default let consonant_string = my_filter(s, isConsonant) // but if you declare the result as a specific type, the // second version will be used: let consonant_array = my_filter(s, isConsonant) as Array
Third, there’s the big gotcha that means this wouldn’t be a good idea, but that I haven’t thought of. If you have, leave a comment or tweet me.
-
or rather,
_ExtensibleCollectionType
, which contains the goods, and thatExtensibleCollectionType
just inherits without any additions. I’m not sure why it’s done this way, though I’m guessing it’s not for no reason. ↩ - This is of course a horrible piece of code, ignoring upper-case characters, not to mention accented characters, but let’s keep the examples simple. ↩
-
Using
lazy(s).filter
is probably more efficient, since it won’t require the construction of temporaryArray
. But the issue of it being a two-step process remains. ↩ - Having written the second version, you should probably implement the first one in terms of the second to avoid code duplication. ↩
[…] But there are two fewer sorted functions. The ones that take a MutableCollectionType, and return one, are gone. Seems a shame, though it didn’t really make sense for them to take a mutable type given they didn’t need to mutate it. Maybe they’ll be replaced with versions that return ExtensibleCollectionType. […]