Published: Oct 12, 2015
This post is also available as a Swift playground at https://github.com/jverkoey/playgrounds.
Let’s build a Dictionary extension that fills in empty keys with “default” values when accessed. This extension will replace the following logic:
if dictionary[key] == nil { dictionary[key] = DefaultValue() } let value = dictionary[key]!
with something that can be expressed in a single line.
#
An implementationPresented below is an extension of Dictionary that defines the mutating func get
. Because we’re always going to return something, we can make this get
function return a non-optional Value
type.
extension Dictionary { mutating func get(key: Key, @autoclosure withDefault value: Void -> Value) -> Value { if self[key] == nil { self[key] = value() } return self[key]! } }
Some interesting things going on here:
value
is a closure so that we may lazily evaluate the parameter.
value
is also an @autoclosure. Autoclosures are a powerful language feature that implicitly turns arguments into closures. For example, rather than writing
fn({SomeObject()})
you write
fn(SomeObject())
and SomeObject()
will be implicitly wrapped in a closure for evaluation by the function at a later point. Use this feature sparingly and with great care.
Now we can write the following:
struct Device { func addObserver() -> String { return "added observer" } } var dictionary: [String: Device] = [:] dictionary.get("foo", withDefault: Device()).addObserver()
#
Array values can’t be modifiedConsider the following:
var dictionary: [String:[String]] = [:] dictionary.get("foo", withDefault: []).append("bar")
If you attempt to run this code in a Playground you’ll get the following error:
Cannot use mutating member on immutable value: function call returns immutable value
If we ignore what it’s saying and try to get it to compile, we might change the code to:
var dictionary: [String:[String]] = [:] var strings = dictionary.get("foo", withDefault: []) strings.append("bar")
But check out what happens when we print out dictionary
:
["foo": []]
Our default array was stored, but “bar” isn’t in it!
Why? Arrays returned by functions in Swift are copies. Modifications made to the return value will not be reflected in the dictionary’s value.
So why, then, does the following code work?
var dictionary: [String:[String]] = [:] dictionary["foo"] = [] dictionary["foo"]?.append("bar") dictionary // result: ["foo": ["bar"]]
After all, we’re using Dictionary’s subscript function — isn’t it copying the returned array which should result in the same behavior as our get
function?
Interestingly enough: it is returning a copy! But there’s some magic going on when we execute dictionary[key]?.append("bar")
:
dictionary
’s subscript get
is called and the corresponding value is returned.
.append("bar")
is then invoked on the returned value. The copy of the array that was returned now contains “bar”.
And here’s the interesting part: subscript set
is called with the modified array.
These three steps give the illusion that we’re modifying the array in-place. Note that, without step 3, the dictionary won’t end up storing the modified array:
var dictionary: [String:[String]] = [:] dictionary["foo"] = [] var array = dictionary["foo"]! // 1. array.append("bar") // 2. // dictionary["foo"] = array // 3. dictionary // result: ["foo": []] when 3 is commented out
#
A better solutionArmed with a better understanding of how in-place dictionary modifications work we can revisit our original implementation.
We’ll use subscript
because it allows for inline get/set semantics. But how do we provide a default value?
Tucked away in the Swift 2 Subscripts documentation is our answer:
Subscripts are not limited to a single dimension, and you can define subscripts with multiple input parameters to suit your custom type’s needs.
So let’s rewrite our method using subscript
:
extension Dictionary { subscript(key: Key, @autoclosure withDefault value: Void -> Value) -> Value { mutating get { if self[key] == nil { self[key] = value() } return self[key]! } set { self[key] = newValue } } }
get
must be marked mutating
in order to modify self.
set
must be implemented in order to support inline value modifications.
And this is how you use it:
dictionary["foo", withDefault: []].append("bar")
Much better!
#
Final notesI learned how subscript in-place modifications work by doing the following:
Created a new Playground.
Wrote a simple class with a subscript get/set implementation.
Added print statements to the get/set.
Wrote a simple example of what I wanted to achieve.
class Foo { var array: [String] = [] subscript(name: String) -> [String] { get { print("get") return array } set { print("set") array = newValue } } } let f = Foo() f["foo"].append("bar")
Much to my surprise, observing the logs revealed:
get set
A drawback to the subscript solution is that autocomplete will not help.