ShopKeep

ShopKeep Engineering

66 public repos
10 members

Immutable values and iOS

Posted by Stepan Hruda, Lead Engineer on Mar 4, 2015  

Recently, the iOS development circles have been pretty busy talking about functional programming concepts. One of those is using immutable values, rather than using mutable objects and sharing references to them. If you’re not completely familiar with the topic, Andy Matuschak gave a wonderful talk about Making Friends with Value Types where he very well explains the advantages of this approach.

It resonates with how we have been thinking about the model layer for our apps, and this was the final tipping point to start fully embracing immutability where possible. It is a significant paradigm shift and doesn’t come without caveats. This is not a bad thing, though – mutability has its own caveats as well (arguably worse ones in the long run), you just already learned how to handle those! Let’s explore some challenges you might face when dealing with immutable values in real life.

Regardless of whether you use Objective-C classes, Swift structs, or anything else to ensure immutability, chances are you will soon run into this question:

struct Person {
    var socialSecurityNumber: String
    var age: Int
    var salary: Int
}

let jack = Person(socialSecurityNumber: "123-45-6789", age: 30, salary: 50000)
let jackAgain = Person(socialSecurityNumber: "123-45-6789", age: 30, salary: 50000)

jack == jackAgain // true or false?

Well, true nor false, at least not in the code above. Swift won’t let you use the == operator, unless you conform to the Equatable protocol. Which in turn means you have to explicitly define the == operator. jack and jackAgain above are definitely equal, though, so let’s define ==.

extension Person: Equatable {}

func ==(lhs: Person, rhs: Person) -> Bool {
    return lhs.socialSecurityNumber == rhs.socialSecurityNumber
        && lhs.age == rhs.age
        && lhs.salary == rhs.salary
}

Seems legit, jack == jackAgain now returns true. How about this case?

extension: Person {   
    func giveRaise() -> Person {
        return Person(socialSecurityNumber: socialSecurityNumber, age: age, salary: salary + 10000)
    }
}

let jack = Person(socialSecurityNumber: "123-45-6789", age: 30, salary: 50000)
let jackAfterAGoodMonth = jack.giveRaise()

jack == jackAfterAGoodMonth // true or false?

Given how we defined == above, this is false. Did we define == incorrectly for Person, then? Are two Persons equal if they have the same socialSecurityNumber and the other values don’t matter? Documentation for protocol Equatable has a clear opinion on this.

/// **Equality implies substitutability**.  When `x == y`, `x` and
/// `y` are interchangeable in any code that only depends on their
/// values.

We can’t use jack and jackAfterAGoodMonth interchangeably, some of their values differ. For example accountingDepartment.sendSalary(person) would yield different results if we pass jack and jackAfterAGoodMonth as an argument. We have defined == correctly.

But you have to agree that although jack and jackAfterAGoodMonth aren’t equal, they refer to the same person. After all, they have the same social security number. That’s because jack and jackAfterAGoodMonth have the same identity, but they hold different values. Both are snapshots of Jack (or Jack’s identity, to be precise) at a certain point in time.

Are you me?

What if we need to know whether two snapshots represent the same identity? Perhaps a controller needs to react differently, if it stops displaying data about one person and switches to another.

class SomeController: UIViewController {
    // you have a controller with this property defined, unless your whole app is written uber-declaratively
    var person: Person {
        get { return self.person }
        set {
            let switchingPersons = self.person.socialSecurityNumber != newValue.socialSecurityNumber
            self.person = newValue
            if (switchingPersons) {
                // special business logic
            }
        }
    }
}

The controller above (and whoever reads the code) has to have context on what defines the given identity, though. They have to understand how social security numbers work and that they’re unique. And they’ll have to learn a new concept of uniqueness for every new identity they’ll ever deal with.

I wonder if we can help here. To the abstraction machine!

func isTheSameIdentity<T: Identifiable>(a: T, b: T) -> Bool {
    return a.identifier == b.identifier
}

let switching = isTheSameIdentity(self.person, newValue)

Now the reader only needs to learn once what identity means and they’ll understand identity everywhere we use the concept of identity. It might seem trivial, but it’s a big win in terms of cognitive load.

By the way, how is Identifiable defined?

protocol Identifiable {
    typealias IdentifierType: Equatable
    var identifier: IdentifierType { get }
}

extension Person: Identifiable {
    typealias IdentifierType = String
	var identifier: IdentifierType { get { return socialSecurityNumber } }
}

Inside our identifiable struct, we define what the unique identifier is, using an associated type. In our case, social security number is a String.

We can typealias IdentifierType to any type, though, so a Person can be uniquely identified by a (strongly typed) pair of values, for example.

struct WorldwidePersonIdentifier {
    let countryCode: String
    let socialSecurityNumber: String

    ...
}

struct Person: Identifiable {
    typealias IdentifierType = WorldwidePersonIdentifier
    var identifier: IdentifierType { get { return worldwidePersonIdentifier } }
}

On a side note, it’d be nice if Swift allowed us to solve this using a generic protocol syntax. In an ideal world, Person would conform to IdentifiableBy<T>.

// This isn't possible, but is effectively the same as using `typealias` as shown above.
extension Person: IdentifiableBy<WorldwidePersonIdentifier> {
    var identifier: WorldwidePersonIdentifier { get { return worldwidePersonIdentifier } }
}

Noteworthy pitfall

It’s important to note that using a set to make sure we only have a single value for an identity won’t work. Uniqueness is ultimately enforced by equality in a set (hashes are just an implementation detail for speed).

Remember when we established that jack and jackAfterAGoodMonth aren’t equal? It also means that we can have both of them in a set.

var persons: Set = []
let jackAfterAGoodMonth = jack.giveRaise()

persons.insert(jack)
persons.insert(jackAfterAGoodMonth) // count 2, ouch, we now have Jack both before and after his raise

Even if we went back and tried redefining jack and jackAfterAGoodMonth to be equal, inserting a new non-unique object into a set is a no-op. So we’d end up only with the original jack in the set and Jack wouldn’t get a raise after all.

Latest and greatest

So we shouldn’t store an object in a Set or an NSSet for uniqueness. We could build our own collection, IdentitySet<V: Identifiable>, which will enforce the uniqueness for us.

In practice, we’ll ideally never need this collection, though. It’s often just fine to pass values around in an array. Chances are, two different values for the same identity will never end up in the same array, as long as our persistence layer ensures the uniqueness. Let’s build such a persistent store, then.

So far, we have been dealing with immutable structs, and suddenly this is a mutable class. It makes sense, because persistence is inherently stateful.

class IdentityStore<V: Identifiable where V.IdentifierType: Hashable> {
    private var storage: [V.IdentifierType:V] = [:]
    
    func all() -> [V] {
        return storage.values.array
    }
    
    func add(value: V) {
        storage[value.identifier] = value
    }
    
    func remove(value: V) {
        storage[value.identifier] = nil
    }
    
    func latest(value: V) -> V? {
        return storage[value.identifier]
    }
}

Simple, but powerful API. We can update the previous value for the identity by calling add, and make sure we have the latest value for an outdated one with latest.

let store = IdentityStore<Person>()
let jack = Person(socialSecurityNumber: "123-45-6789", age: 30, salary: 50000)
let jackAfterAGoodMonth = jack.giveRaise()

store.add(jack) // count 1
store.add(jackAfterAGoodMonth) // count 1, salary updated
store.latest(jack) // 60000 salary, even though we're asking with an out-of-date value

Real life persistence

For our last trick, we’ll switch the in-memory dictionary for a key-value store, which can provide persistence usable in a real app. Let’s store our values in NSUserDefaults.

This requires a little dance, because as a generic Swift dictionary, [V.IdentifierType:V] cannot be straight up stored to NSUserDefaults. The details of the serialization are irrelevant to this article, so we’re not going to spend time on them. The functionality that becomes available in the end is much more interesting.

class IdentityStore<V: Identifiable where V: Serializable> {
    private var defaults: NSUserDefaults
    private var key: String

    private var storage: [V.IdentifierType:V] {
    get {
        let dictionary = defaults.objectForKey(key) as NSDictionary
        return deserializeNSDictionary(dictionary, V.self)
    }
    set { defaults.setObject(serializeSwiftDictionary(newValue), forKey: key) }
    }
    
    init(key: String) {
        self.defaults = NSUserDefaults.standardUserDefaults()
        self.key = key
        let contents: AnyObject? = defaults.objectForKey(key)
        if contents == nil {
            defaults.setObject([:], forKey: key)
        }
    }
}

Now we can have multiple light-weight persistent stores, differentiated by a unique key, backed by NSUserDefaults. The results they give us will never mutate (this is directly ensured by Swift’s value types, but the same can be achieved in Objective-C), and can be completely safely shared between threads.

let jack = Person(socialSecurityNumber: "123-45-6789", age: 30, salary: 50000)
let jackAfterAGoodMonth = jack.giveRaise()

let store = personStore()

store.add(jack)
store.add(jackAfterAGoodMonth)

let differentStore = personStore()
differentStore.latest(jack) // gets us Jack with 60000 salary

We could also modify the identity store to point to a specific NSUserDefaults suite, and share our data between apps and extensions!

You’d probably want more functionality from an identity store used in production. I hope I illustrated some basic challenges of using immutable values and got rid of the anxiety to make the leap and start using them.

If you find the article interesting, you can follow me on Twitter, I’m more than happy to keep the discussion going. We’re also hiring at ShopKeep – a growing team (~15 iOS developers), stacks in Go and Ruby and offices in NYC, Belfast and Portland. The code discussed above is available as a gist.