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?
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.
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)
}
}
}
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)
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With