featherless software design

View on GitHub
12 October 2015

Swift Dictionary Get With Default

by

Swift Dictionary: get with default

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 implementation

Presented 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:

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 modified

Consider 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"):

  1. dictionary’s subscript get is called and the corresponding value is returned.
  2. .append("bar") is then invoked on the returned value. The copy of the array that was returned now contains “bar”.
  3. 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 solution

Armed 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
    }
  }
}

And this is how you use it:

dictionary["foo", withDefault: []].append("bar")

Much better!

Final notes

I learned how subscript in-place modifications work by doing the following:

  1. Created a new Playground.
  2. Wrote a simple class with a subscript get/set implementation.
  3. Added print statements to the get/set.
  4. 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
tags: