Generic Protocol Extension

Is there a way to make a generic extension to a protocol?


As an example take a filterNils method, that removes all nil elements from a collection and unwraps the rest:


extension CollectionType where Generator.Element == Optional<AnyObject> {
    func filterNils() -> [AnyObject] {
        return self.filter { $0 != nil }.map { $0! }
    }
}


This code snippet only works for variables of type Array<AnyObject?> though, not for e.g. Array<Int?>.


What I really want to achieve would be along the lines of this:

extension CollectionType where Generator.Element == Optional<T> {
    func filterNils() -> [T] {
        return self.filter { $0 != nil }.map { $0! }
    }
}


But that is not valid Swift 2.0 code. Any suggestions how that can be achieved without using parameter generics like in this solution [2]?


[2] http://stackoverflow.com/a/28190873/354018

Accepted Reply

The challenge is that Optionals don't conform to a particular protocol. That can be solved by creating a new OptionalType protocol that declares flatMap and extending Optional for conformance:


protocol OptionalType { 
    typealias T
    func flatMap<U>(@noescape f: (T) -> U?) -> U?
}

extension Optional : OptionalType { }


Then you can add an extension to Array (or better, SequenceType):


extension SequenceType where Generator.Element: OptionalType {
    func flatten() -> [Generator.Element.T] {
        return self.map { $0.flatMap { $0 } }
                   .filter { $0 != nil }
                   .map { $0! }
    }
}
let mixed: [Int?] = [1, 2, nil, 3, nil, 4]
let numbers = mixed.flatten()    // 1, 2, 3, 4

Replies

The challenge is that Optionals don't conform to a particular protocol. That can be solved by creating a new OptionalType protocol that declares flatMap and extending Optional for conformance:


protocol OptionalType { 
    typealias T
    func flatMap<U>(@noescape f: (T) -> U?) -> U?
}

extension Optional : OptionalType { }


Then you can add an extension to Array (or better, SequenceType):


extension SequenceType where Generator.Element: OptionalType {
    func flatten() -> [Generator.Element.T] {
        return self.map { $0.flatMap { $0 } }
                   .filter { $0 != nil }
                   .map { $0! }
    }
}
let mixed: [Int?] = [1, 2, nil, 3, nil, 4]
let numbers = mixed.flatten()    // 1, 2, 3, 4

Wow, thank you. I still try to comprehend it fully though. What is flatMap needed for? Is this a hack to get the generic type of the optional?

`flatMap` is just how you get from a generic `OptionalType`, which has no useful semantics aside from `flatMap`, to an `Optional<T>`, which you can then compare to `nil`, unwrap, etc... Types other than `Optional<T>` might plausibly implement `OptionalType`, so you need to make the conversion.

I don't like this particular implementation, since it handles the other plausible implementors of `OptionalType` incorrectly. If `Generator.Element` is anything other than `Optional<Generator.Element.T>`, then `{ $0 }` will be `Generator.Element -> Optional<Generator.Element>` instead of `Generator.Element -> Generator.Element`, i.e. it'll be equivalent to `Some` instead of `id`. I'm actually surprised that the special case is handled at all here, actually (haven't tested it).


If you could impose the additional constraint that `Generator.Element == Optional<Generator.Element.T>`, then you would solve this, and potentially eliminate the need for `flatMap`, though I'm unable to test whether this works at the moment. Alternatively, you could have an `intoOptional` method in `OptionalType` which bypasses the need to supply a `Generator.Element -> Optional<Generator.Element.T>` function to `flatMap`.

Ok, I think I understand:


  • In a where clause, it is currently not possible to include all generic versions of Optional<>
  • But it is possible to include all implementers of a certain protocol
  • So, as a workaround, the OptionalType protocol can be created, and Optional can be extended with it
  • Now when including the OptionalType protocol in a where clause, all generic versions of Optional<> will be included (but other implemeters of that protocol too)
  • As it is not guaranteed that an implemeter of OptionalType actually is an Optional<>, the method flatMap needs to be used, which will definitely return an optional "U?"
  • In flatten, the flatMap function will move the return type from Generator.Element to Generator.Element.T?
  • Then filter and map are used to filter out nils and unwrap the remaining Some values
  • Generator.Element.T is provided as a typealias by OptionalType, which just makes the generic type of Optional<T> accessible


>> I don't like this particular implementation, since it handles the other plausible implementors of `OptionalType` incorrectly. If `Generator.Element` is anything other than `Optional<Generator.Element.T>`, then `{ $0 }` will be `Generator.Element -> Optional<Generator.Element>` instead of `Generator.Element -> Generator.Element`, i.e. it'll be equivalent to `Some` instead of `id`.


Could you give an example of a working implementation where `Generator.Element` is anything other than `Optional<Generator.Element.T>`? I tried but could not come up with one.


>> If you could impose the additional constraint that `Generator.Element == Optional<Generator.Element.T>`...


Seems not to work, the Playground crashes when I enter this.


>> Alternatively, you could have an `intoOptional` method in `OptionalType` which bypasses the need to supply a `Generator.Element -> Optional<Generator.Element.T>` function to `flatMap`.


That's a good idea, works. Here is the full code:

protocol OptionalType {
    typealias T
    func intoOptional() -> T?
}

extension Optional : OptionalType {
    func intoOptional() -> T? {
        return self.flatMap {$0}
    }
}

extension SequenceType where Generator.Element: OptionalType {
    func flatten() -> [Generator.Element.T] {
        return self.map { $0.intoOptional() }
            .filter { $0 != nil }
            .map { $0! }
    }
}

let mixed: [AnyObject?] = [1, "", nil, 3, nil, 4]
let nonnils = mixed.flatten()    // 1, "", 3, 4

> Could you give an example of a working implementation where `Generator.Element` is anything other than `Optional<Generator.Element.T>`? I tried but could not come up with one.


Turns out not to matter. I messed up that part 😉. Because flatmap takes a T -> U?, it doesn't need you to provide it with a Generator.Element -> Generator.Element.T?—which is what I was saying wouldn't work—but instead a Generator.Element.T -> Generator.Element.T?, which means the implementation using flatMap will always work, since you can always write that function as {$0}. Sorry for any confusion my confusion caused.


> Seems not to work, the Playground crashes when I enter this.


Bummer. I was hoping you could use the associated type introduced by the OptionalType protocol to supply the type argument to Optional<>.Since it seems you can't, that means you can't make the extension more closely match your original intent, or remove the extraneous .map { $0.flatMap { $0 } } (or .map { $0.intoOptional() })which would be a no-op if you were guaranteed to always be working with Optional<>s. Hopefully it can be optimized away in that case, though I'm not sure the optimizer is that sophisticated.


Since the playground crashes, though, that means we're encountering a bug, regardless of whether it should work or not, so it would probably be good to report that.

Submitted crash: http://www.openradar.me/21436336