5

Using this Stack Overflow question I have the following code.

let numbers = [1,[2,3]] as [Any]
var flattened = numbers.flatMap { $0 }
print(flattened) // [1, [2, 3]]

Instead of flattened being set to [1, [2, 3]] I want it to be [1, 2, 3].

What is the easiest/cleanest way to achieve this in Swift?

Charlie Fish
  • 18,491
  • 19
  • 86
  • 179
  • 1
    `flatMap` works with an array of arrays, not an array of `Any`. – rmaddy Nov 29 '17 at 03:43
  • @rmaddy Yeah I figured that out, that makes sense to me now. Is there anyway to achieve what I want? Is there some alternative to `flatMap` that will help me achieve what I want? – Charlie Fish Nov 29 '17 at 03:45
  • Possibly solution here: https://stackoverflow.com/questions/42587629/swift-function-taking-generic-array/42599849#42599849 – Martin R Nov 29 '17 at 06:04
  • Is it so unreasonable to think that flatMap should work like this? If you want to flatten a tree you start with an array that has nodes and subarrays for each node. I don't think this should require an extension. In any case, thanks for the solutions, all! – Andy Weinstein May 24 '21 at 10:07

3 Answers3

7
extension Collection where Element == Any {
    var joined: [Any] { flatMap { ($0 as? [Any])?.joined ?? [$0] } }
    func flatMapped<T>(_ type: T.Type? = nil) -> [T] { joined.compactMap { $0 as? T } }
}

let objects: [Any] = [1,[2,3],"a",["b",["c","d"]]]
let joined = objects.joined()   // [1, 2, 3, "a", "b", "c", "d"]

let integers = objects.flatMapped(Int.self)  // [1, 2, 3]
// setting the type explicitly
let integers2: [Int] = objects.flatMapped()        // [1, 2, 3]
// or casting
let strings = objects.flatMapped() as [String]     // ["a", "b", "c", "d"]
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Would I be able to do the same thing for Strings? – Charlie Fish Nov 29 '17 at 03:51
  • In the event that I am mixing types (not happening now, but curious for future reference) how would that work? – Charlie Fish Nov 29 '17 at 03:53
  • 2
    Note that `lazy` is redundant in both cases as you're using the overloads that return arrays (i.e eagerly evaluating). You could rephrase `($0 is T ? [$0 as! T] : [])` as `($0 as? T).map { [$0] } ?? []`, though personally I would probably use a `switch` (e.g https://gist.github.com/hamishknight/eca9b0be62056284ec37c3d49dd7db65). Also, personally I would make `flattened` a method, as it's not O(1); though I know you like your computed properties :) – Hamish Nov 29 '17 at 21:52
7

There may be a better way to solve this but one solution is to write your own extension to Array:

extension Array {
    func anyFlatten() -> [Any] {
        var res = [Any]()
        for val in self {
            if let arr = val as? [Any] {
                res.append(contentsOf: arr.anyFlatten())
            } else {
                res.append(val)
            }
        }

        return res
    }
}

let numbers = [1,[2, [4, 5] ,3], "Hi"] as [Any]
print(numbers.anyFlatten())

Output:

[1, 2, 4, 5, 3, "Hi"]

This solution will handle any nesting of arrays.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 1
    You can implement this with a flatMap expression: `self.flatMap{ ($0 as? [Any]).map{ $0.anyFlatten() } ?? [$0] }` – Alexander Nov 29 '17 at 04:32
  • @Alexander That does work, thanks. But it is really inefficient it seems compared to my less "slick" implementation. – rmaddy Nov 29 '17 at 04:38
  • 1
    Where's the inefficiency? Beyond the overhead of closures (which will be completely optimized away in this case) – Alexander Nov 29 '17 at 04:44
  • @Alexander In a playground testing with the `numbers` array in my answer, the playground shows my `return` statement being called 3 times. When I use your one-line implementation of your `anyFlatten` method, the playground shows the `return` being called 13 times for the same input. Based on that, it seems inefficient. But I haven't done any rigorous tests. – rmaddy Nov 29 '17 at 04:50
  • 3
    If I understand your testing method correctly, I think it's quite wrong. Your code's `return` occurs at the end of every recursive call. The number of time your `return` statement is called is equal to the sum of depths of all subarrays. In my code, `return` has the effect of the `.append(contentsOf:)` and `append(_:)` calls in your code. Flattening `Array(0..<10)` (10 elements, all depth 1) would call `return` 10 times for me (10 elements), and once for you (max depth is 1), but that count measures completely different things – Alexander Nov 29 '17 at 04:56
5

Here's an alternate implementation of @rmaddy's anyFlatten:

It can be most concisely written like so, but it's quite cryptic:

extension Array {
    func anyFlatten() -> [Any] {
        return self.flatMap{ ($0 as? [Any]).map{ $0.anyFlatten() } ?? [$0] }
    }
}

Here's a more reasonable implementation:

extension Array {
    func anyFlatten() -> [Any] {
        return self.flatMap{ element -> [Any] in
            if let elementAsArray = element as? [Any] { return elementAsArray.anyFlatten() }
            else { return [element] }
        }
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151