3
\$\begingroup\$
extension Array {
    func flatten<TResult>(transform: (T) -> TResult?, childArray: (T) -> [T]) -> [TResult] {
        var result = [TResult]()

        for original in self {
            let transformed = transform(original)
            if transformed != nil {
                result.append(transformed!)
            }
            result += childArray(original).flatten(transform, childArray: childArray)
        }

        return result
    }
}

Example usage to get all UILabels in a view (including it's subviews) :

let labels = someView.subviews.flatten(
                        { $0 as? UILabel },
                        childArray: { $0.subviews })

The code can probably be simplified/optimized, but how?

\$\endgroup\$
  • \$\begingroup\$ Please have a look at What you may and may not do after receiving answers. In particular you should not append your revised code to the question. You can post a follow-up question instead. – I have therefore rolled back you last edit. \$\endgroup\$ – Martin R Apr 15 '15 at 8:09
3
\$\begingroup\$

Let's start with the method and parameter names (this part is highly opinion based :)

flatten() does not describe well what you method does: It traverses recursively a tree-like structure and maps the elements to some values. Swift already has a flatMap() function (and similar methods for arrays and other types) which does a similar thing, but only one level deep. Therefore I would call the method recursiveFlatMap().

Also the descendants of a node in a tree are usually called "children", so I would choose that as name for the second parameter. (And perhaps you want to generalize the code for other types, such as sequences.)

for original in self { ... }

iterates of the array elements, therefore I would write that as

for element in self { ... }

let transformed = transform(original)
if transformed != nil {
     result.append(transformed!)
}

can be simplified with optional binding:

if let transformed = transform(element) {
    result.append(transformed)
}

Then we have:

extension Array {
    func recursiveFlatMap<TResult>(transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
        var result = [TResult]()
        for element in self {
            if let transformed = transform(element) {
                result.append(transformed)
            }
            result += children(element).recursiveFlatMap(transform, children: children)
        }
        return result
    }
}

Now let's have a look how the method is called:

let labels = someView.subviews.recursiveFlatMap(
                { $0 as? UILabel },
                children: { $0.subviews })

What strikes me is that you have to specify subviews twice: In someView.subviews and in $0.subviews. If the method were called on someView itself then we would have to specify only once how to descend to the "child nodes".

This means that instead of an Array extension method we have a (free) generic function which takes the "root of the tree" as the first argument:

func recursiveFlatMap<T, TResult>(root: T, transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
    var result = [TResult]()
    if let value = transform(root) {
        result.append(value)
    }
    for child in children(root) {
        result += recursiveFlatMap(child, transform, children)
    }
    return result
}

The for-loop can be replaced by the built-in flatMap() method:

func recursiveFlatMap<T, TResult>(root: T, transform: (T) -> TResult?, children: (T) -> [T]) -> [TResult] {
    var result = [TResult]()
    if let value = transform(root) {
        result.append(value)
    }
    result += children(root).flatMap( { recursiveFlatMap($0, transform, children) } )
    return result
}

The function is called as

let labels = recursiveFlatMap(someView,
                { $0 as? UILabel },
                { $0.subviews as! [UIView] })

Finally, you can mark the two closure parameters with the @noescape attribute. This is new in Swift 1.2 (described in the Xcode 6.3 release notes) and tells the compiler that the closure is used only during this function call and does not outlive the lifetime of the call. This allows some compiler optimizations:

func recursiveFlatMap<T, TResult>(root: T,
    @noescape transform: (T) -> TResult?,
    @noescape children: (T) -> [T]) -> [TResult]
{
    var result = [TResult]()
    if let value = transform(root) {
        result.append(value)
    }
    result += children(root).flatMap( { recursiveFlatMap($0, transform, children) } )
    return result
}

More thoughts (inspired by this blog post):

recursiveFlatMap() does actually three things:

  • Recursively traverse a tree-like structure,
  • transform each object, and
  • select the objects for which the transformation succeeded.

Swift has built-in methods for the second and third task: map() and filter(). So you could just define a function that recursively flattens the given object (in your case the tree of subviews):

func recursiveFlatten<T>(root: T, @noescape children: (T) -> [T]) -> [T] {
    return [root] + children(root).flatMap( { recursiveFlatten($0, children) } )
}

and then use it as

let labels = recursiveFlatten(someView, { $0.subviews as! [UIView] })
    .filter( { $0 is UILabel } )
    .map( { $0 as! UILabel } )

The advantage is that you have a simpler function and a clear separation into different tasks.

There is a small drawback: an array of all subviews is created first, and then the labels are extracted. But that is probably tolerable if the number of subviews is not too large.

(One could work around this problem by defining a method which returns a "lazy sequence", posted separately as Recursive flattening of Swift sequences.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.