On twitter, @rob_rix confessed that he’d given up and used a for loop. @rbrockerhoff replied he had too, in his case because you can’t create a dictionary from an array.
That tweet sent me deep down the rabbit hole experimenting with Dictionary
. I keep wanting to fix it, like a scruffy boyfriend. But like with said boyfriend, that’s easier said than done.
Warning: some of the below is very experimental, thoroughly likely to crash your playground, and quite possibly wrongheaded in lots of places. If you spot any of the latter, do tweet me a correction @airspeedswift.
The root of the problem is that, unlike Array
, Dictionary
doesn’t have an initializer that takes a Sequence
of key/value tuples. If it did, you could do cool stuff like this:
let words = ["zero", "one","two"] let enumerated = enumerate(words) let number_to_word = Dictionary(enumerated) // results in [0:"zero", 1:"one", 2:"two"] let d = [0:"zero", 1:"one", 2:"two"] let flipped = Dictionary(Zip2(d.values, d.keys)) // results in ["zero":0, "one":1, "two":2]
Now, if you root around in the standard library1 you’ll find it does have this function, which it implements as part of the DictionaryLiteralConvertible
protocol:
static func convertFromDictionaryLiteral (elements: (KeyType, ValueType)...) -> [KeyType : ValueType]
This is tantalizingly close to what we want, at least for arrays if not sequences. It takes a variadic argument of tuples, which will in the function itself be represented as an array of tuples. Hey, we want to create a dictionary from an array of tuples! Can we use this?
Nope. Try as you might, you can’t shove an array through that function. If you managed to get one in, inside the function it would appear as an array inside an array. Languages like Ruby and Python have the ability to “splat” an array into a function as its parameters, but (as far as I can tell) there’s no way to do that in Swift.
Sigh. I guess there’s no alternative but to use a loop. But to make myself feel better about it, I’ll fix something else that bugs me about Dictionary
. As we saw, convertFromDictionaryLiteral
takes an array of (Key, Value)
tuples. But the one function in Dictionary
that also takes a key and a value takes them the other way around:
func updateValue(value: ValueType, forKey key: KeyType) -> ValueType?
You know how I said you can’t pass an array into a variadic argument in Swift? Well, you can pass a tuple into a function that has the same number of arguments as the tuple:
func takeTwoArgs(arg1: Int, arg2: String) { // do stuff with arg1 and arg2 } let pair = (1, "one") takeTwoArgs(pair)
To take an array of tuples and insert them into a dictionary would be a lot nicer if you could just pass the elements of the array in directly, rather than having to flip them around. This will do it:
extension Dictionary { // offensive vague naming of a function alert... mutating func update (key: KeyType, _ val: ValueType) -> ValueType? { return self.updateValue(val, forKey: key) } } let pairs = [(0,"zero"), (1,"one"), (2,"two")] var d: [Int:String] = [:] for pair in pairs { d.update(pair) }
OK, doing that makes me feel a bit more cheerful, but I’d still like to be able to construct a dictionary from scratch using an initializer. This isn’t just a whim, by the way – having to do the for loop above forces you to declare d
with var
, whereas an initializer could be used with let
.
So how about we extend Dictionary
with an initializer:
extension Dictionary { init(_ array: [Element]) { for pair in array { self.update(pair) } } }
Oops, a mysterious compiler error. “Variable 'self._variantStorage' used before being initialized
“. We’ve accidentally been exposed to the inner implementation of Dictionary
. And unlike Array
, which has some hints at how you might manipulate the underlying store with structs like ArrayBuffer
, Dictionary
is entirely enigmatic. We could try and figure out how to initialize _variantStorage
by hand but that’s just going to lead to tears.
Or, we could just create another Dictionary
, populate it, and then assign it to self
right at the end of the initializer:
edit: Ignacio in the comments points out the (now blindingly obvious) alternative of just calling self.init()
instead – thanks! But I leave the self-assignment here as something of interest that works.
edit edit: Maybe not so fast. self.init() works in playground, but not in compiled code right now.
extension Dictionary { init(_ array: [Element]) { var d: [KeyType:ValueType] = [:] for pair in array { d.update(pair) } self = d } }
I have no idea if this is supposed to be allowed or not. I mean, it seems to work fine, but I think I might have voided the warranty. Certainly I’m going to think twice about filing this code as part of a radar.
Nevertheless, we can now do this:
let pairs = [(0,"zero"), (1,"one"), (2,"two")] let d = Dictionary(pairs) // woo-hoo // d now contains [0:"zero", 1:"one", 2:"two"]
For my last trick, I would love to say I’ve generalized this to work for sequences, but unfortunately I still can’t figure that one out. This seems like it ought to be the solution:
extension Dictionary { init<S: Sequence where S.GeneratorType.Element == Element> (_ seq: S) { var d = [KeyType:ValueType] = [:] for pair in seq { d.update(pair) } self = d } }
but it segfaults the compiler (I need to find a way to reproduce this without the dodgy self-assignment, so I can file a radar with less silly code…) (edit: well, now I know a way! Was hoping that would fix the segfault, but no)
As a workaround, I’ve tried creating an array with the sequence and then using the previous initializer:
extension Dictionary { init<S: Sequence where S.GeneratorType.Element == Element> (_ seq: S) { var d = Dictionary(Array(seq)) self = d } }
However, this leads to the compiler compliaining that “'WhateverYourSequenceTypeIs' is not convertible to '[(KeyType, ValueType)]'
“. This suggests it’s not matching the sequence initializer. If anyone can spot what’s wrong here and has a fix, let me know. For now, just stick an Array()
around your init
arguments and pretend you’re in for-loop free nirvana.
edit: found it! Read the next post for a solution.
-
In case you don’t know how to do that, type
import Swift
into a playground then command-click on the word Swift. ↩