Array extension where Element is a class

Is it possible to constrain an extension to where an associated type is a class, similar to the : class constraint on protocols?

For my specific situation, I had this:

extension Array
{
  func firstResult<T>(_ predicate: (Element) -> T?) -> T?
  {
    for element in self {
      if let result = predicate(element) {
        return result
      }
    }
    return nil
  }

  func firstOfType<T>() -> T? where T: Element
  {
    return firstResult { $0 as? T }
  }
}

Of course, firstOfType has an error: "Type 'T' constrained to non-protocol, non-class type 'Element'" because you can't say that T is a subclass of Element when there is no constraint that says Element is a class.

I tried moving firstOfType to its own extension Array where Element: class assuming that would work like it does with protocols, but got an "Expected type" error on class there. So is there a way to do that constraint? I suppose I could make it work by naming a specific class, but I'd rather have a more general solution.

It also works fine if I just omit the where T: Element constraint, I'd just like to have that compile-time check.

I'm not sure about how to solve the problem you lay out, but are you sure it even needs to be solved, at all? You could just do this with existing functions: array.lazy.map(somePredicate).first { $0 != nil }

The correct way of expressing a class constraint here is extension Array where Element: AnyObject, but that's not the main problem.

The compiler doesn't have a way to model a constraint expressing inheritance from or conformance to an archetype today. From a generic point of view, your example is equivalent to what you would expect from

class A {}
class Foo<T: A> {
  func foo<R: T>(_ arg: R) {}
}          ~~~~

That returns a double optional, T??. You could add ?? nil to flatten the wrapped optional.

Maybe this does the job?

extension Sequence {
    func firstResult<T>(_ predicate: (Element) -> T?) -> T? {
        return withoutActuallyEscaping(predicate) { escapingPredicate in
            return self.lazy.compactMap(escapingPredicate).first
        }
    }
    func firstOfType<T: AnyObject>() -> T? {
        return firstResult { $0 as? T }
    }
}
1 Like

Ooops, that map should have been a compactMap. Easy: return self.lazy.compactMap(somePredicate).first

1 Like

Unless you explicitly use an escaping closure as the predicate, you'll get the compactMap that returns an array. It will walk the entire sequence before doing the .first, and thus negating the whole .lazy part.

2 Likes

Interesting!

LazySequenceProtocol offers this implementation of compactMap:

func compactMap<ElementOfResult>(_ transform: @escaping (Self.Elements.Element) -> ElementOfResult?) -> LazyMapSequence<LazyFilterSequence<LazyMapSequence<Self.Elements, ElementOfResult?>>, ElementOfResult>

Notice that its argument is declared as @escaping. LazySequenceProtocol inherits from Sequence, which has this implementation of compactMap:

func compactMap<ElementOfResult>(_ transform: (Self.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

Notice that its argument isn't escaping, and also that it returns an array. In fact, that's the whole reason it doesn't need to escape, since it walks the entire sequence and transforms every element before returning the array. The lazy equivalent defers execution of the transform until you actually walk the lazy sequence later. It therefore needs to escape.

If you simply call .compactMap(transform) on a lazy sequence, the Swift compiler will try to infer which version you're trying to use. If you pass in a non-esacping closure, it'll give you the non-lazy version, negating the whole .lazy part.

However, there's no need to let this requirement leak through the API abstraction, when chaining with a .first, since that'll just evaluate the first element of the sequence and then terminate. Therefore the firstResult function doesn't need to have its argument be escaping. But if you don't mark it as such, you'll get the non-lazy version.

The solution is to temporarily cast the non-escaping closure into an escaping one, but make sure you're done using by the time you return from your scope. This is exactly what withoutActuallyEscaping does.

4 Likes
Terms of Service

Privacy Policy

Cookie Policy