Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Opposite of Swift `zip` — split tuple into two arrays

I have an array of key-value pairs:

let arr = [(key:"hey", value:["ho"]), (key:"ha", value:["tee", "hee"])]

I'm splitting it into two arrays, like this:

let (keys, values) = (arr.map{$0.key}, arr.map{$0.value})

Effectively, that's the opposite of zip — I'm turning an array of tuples into two arrays.

But I don't like the fact that I'm calling map twice, because that means I'm looping through the array twice. Yet neither do I want to declare the two target arrays beforehand as empty arrays and loop once while appending, e.g. with forEach. Is there some wonderful Swifty idiom for unzipping my array of tuples into two arrays?

like image 938
matt Avatar asked Sep 04 '17 20:09

matt


3 Answers

In Swift 4, you can use reduce(into:):

let (keys, values) = arr.reduce(into: ([String](), [[String]]())) {
    $0.0.append($1.key)
    $0.1.append($1.value)
}

You said:

Yet neither do I want to declare the two target arrays beforehand as empty arrays and loop once while appending, e.g. with forEach.

Personally, that's precisely what I would do. I would just write a function that does this (that way you're not sprinkling your code with that pattern). But I think the following is much more clear and intuitive than the reduce pattern, but doesn't suffer the inefficiency of the dual-map approach.

/// Unzip an `Array` of key/value tuples.
///
/// - Parameter array: `Array` of key/value tuples.
/// - Returns: A tuple with two arrays, an `Array` of keys and an `Array` of values.

func unzip<K, V>(_ array: [(key: K, value: V)]) -> ([K], [V]) {
    var keys = [K]()
    var values = [V]()

    keys.reserveCapacity(array.count)
    values.reserveCapacity(array.count)

    array.forEach { key, value in
        keys.append(key)
        values.append(value)
    }

    return (keys, values)
}

Or, if you feel compelled to make it an extension, you can do that, too:

extension Array {

    /// Unzip an `Array` of key/value tuples.
    ///
    /// - Returns: A tuple with two arrays, an `Array` of keys and an `Array` of values.

    func unzip<K, V>() -> ([K], [V]) where Element == (key: K, value: V) {
        var keys = [K]()
        var values = [V]()

        keys.reserveCapacity(count)
        values.reserveCapacity(count)

        forEach { key, value in
            keys.append(key)
            values.append(value)
        }

        return (keys, values)
    }
}

Implement this however you'd like, but when you have it in a function, you can favor clarity and intent.

like image 196
Rob Avatar answered Oct 21 '22 22:10

Rob


Swift 4

reduce(into:) is great, but don't forget to reserveCapacity to prevent reallocation overhead:

extension Array {
    func unzip<T1, T2>() -> ([T1], [T2]) where Element == (T1, T2) {
        var result = ([T1](), [T2]())

        result.0.reserveCapacity(self.count)
        result.1.reserveCapacity(self.count)

        return reduce(into: result) { acc, pair in
            acc.0.append(pair.0)
            acc.1.append(pair.1)
        }
    }
}

Prior to Swift 4

I would apply the KISS principle:

extension Array {
    func unzip<T1, T2>() -> ([T1], [T2]) where Element == (T1, T2) {
        var result = ([T1](), [T2]())

        result.0.reserveCapacity(self.count)
        result.1.reserveCapacity(self.count)

        for (a, b) in self {
            result.0.append(a)
            result.1.append(b)
        }

        return result
    }
}

let arr = [
    (key: "hey", value: ["ho"]),
    (key: "ha",  value: ["tee", "hee"])
]

let unzipped = (arr as [(String, [String])]).unzip()
print(unzipped)
like image 41
Alexander Avatar answered Oct 21 '22 22:10

Alexander


Not pretty but the only thing I could come up with right now: using reduce:

let (keys, values) = arr.reduce(([], [])) { ($0.0.0 + [$0.1.key], $0.0.1 + [$0.1.value]) }

Would be a lot prettier without having to specify the initial values which add a lot of noise and make the code not easily.

Generified it already looks a bit cleaner:

func unzip<K,V>(_ array : [(K,V)]) -> ([K], [V]) {
    return array.reduce(([], [])) { ($0.0 + [$1.0], $0.1 + [$1.1])}
}

let (keys, values) = unzip(arr)
like image 32
luk2302 Avatar answered Oct 21 '22 22:10

luk2302