[stdlib] Enhancements to KeyValuePairs

I use the KeyValuePairs type a lot. Most recently I wanted to use it when constructing some URLComponents and making sure that the query parameters were in the correct order and properly escaped:

extension URLComponents {
    
    mutating func setQueryItems(_ items: KeyValuePairs<String, String?>) {
        queryItems = items.map { key, value in
            return URLQueryItem(name: key,
                                value: value?.addingPercentEncoding(
                                    withAllowedCharacters: .urlQueryAllowed))
        }
    }
    
}

Now, this works fine, but it got me to thinking that it might be cleaner to express the percent encoding as its own step. I wrote a quick extension to KeyValuePairs to map over the values and return an array of tuples:

extension KeyValuePairs {
    
    /// Returns an array containing the results of mapping the given closure
    /// over the sequence’s values.
    ///
    /// - Parameter transform: A mapping closure. `transform` accepts an value
    ///                        of this sequence as its parameter and returns a
    ///                        transformed value of the same or of a different
    ///                        type.
    /// - Returns: An array containing the transformed elements of this
    ///            sequence.
    func mapValues<T>(
        _ transform: (Value) throws -> T
        ) rethrows -> [(key: Key, value: T)] {
        return try map { key, value in return (key, try transform(value)) }
    }
    
}

This allows me to split the original URLComponents extension into two steps for readability:

extension URLComponents {
    
    mutating func setQueryItems(_ items: KeyValuePairs<String, String?>) {
        queryItems = items.mapValues {
            $0?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
            }
            .map(URLQueryItem.init(name:value:))
    }
    
}

What I would really like to do is to write these methods to return KeyValuePairs instead of Arrays:

func map<T, U>(_ transform: (Key, Value) throws -> (key: T, value: U)) rethrows -> KeyValuePairs<T, U>
func mapKeys<T>(_ transform: (Key) throws -> T) rethrows -> KeyValuePairs<T, Value>
func mapValues<T>(_ transform: (Value) throws -> T) rethrows -> KeyValuePairs<Key, T>

(I would also probably write the corresponding compactMap versions.)

Unfortunately, with what’s exposed in the standard library, we can’t do that today. I would at the very least need an append(_:) method to add a new element to a KeyValuePairs instance. I know the KeyValuePairs type is intended to be a very lightweight ordered collection of key-value pairs, but is there any interest in adding some or all of this functionality to the standard library version?

2 Likes

This might be better served by having a proper OrderedDictionary in the standard library, as KeyValuePairs also lacks key subscripting and other features usually seen on dictionary types. Even if it was fully fleshed out I think the only advantage it would have over OrderedDictionary would be the lack of a Hashable constraint on the Key type, though that would largely prevent efficient key subscripting anyway.

2 Likes

It could be that many things using key/value pairs would be served well by an ordered dictionary, but the URL spec says that it’s perfectly valid to have repeated keys with multiple values. It really needs to be represented as a sequence of key/value pairs.

5 Likes

Even just getting append() would help a lot, if you need to conditionally build a sequence of key-value pairs where order matters.