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