Immutable Types in Swift

Object-Orientation and Functional Programming are different solutions to the same problem: Dealing with state in complex systems. Both had identified global mutable state as the source of recurring issues and headaches.

While OOP tackles that problem by hiding mutable state inside of objects and this state should be only accessible through the object’s public interface, FP doesn’t hide the state, rather discourages mutable state. Effectively OOP deleted “global” from “global mutable state” while FP erased “mutable”.

For many years these solutions had been equivalent, and it is fair to say that OOP dominated the scene while FP slowly but steadily grew its discipleship in niche markets like high performance computing and telecommunications.

Then multicore machines became widely available — and that changed everything. Suddenly one of the solutions became better than the other. While functional programs pretty much were able to use any number of cores simply because of the absence of possible side effects created by the hidden state, concurrency is an issue not generally solved for OOP. Different OOP systems offer different solution, like threads and (purpose defeating) locks, dispatching, value over reference types, favouring enumerations of collections over plain iterating.

Well, I think another solution in OOP can be: Copying FP’s solution.

This is a proposal for using fully immutable datatypes in Swift

By “immutable state” we mean that once an object is initialized, its contents cannot be changed; rather if a new value need to be reflected, a new object is created from the old one with the value being replaced.

Let’s look at some example:

struct TodoItem {
    init(text: String) {
        self.init(text: text, completed: false, dueDate: nil, creationDate: Date())
    }

    private
    init(text: String, completed: Bool, dueDate: Date?, creationDate: Date) {
        self.text = text
        self.completed = completed
        self.dueDate = dueDate
        self.creationDate = creationDate
    }
    let text: String
    let completed: Bool
    let dueDate: Date?
    let creationDate: Date

    func set(text:String)    -> TodoItem { TodoItem(text: text, completed: completed, dueDate: dueDate, creationDate: creationDate) }
    func set(completed:Bool) -> TodoItem { TodoItem(text: text, completed: completed, dueDate: dueDate, creationDate: creationDate) }
    func set(dueDate:Date)   -> TodoItem { TodoItem(text: text, completed: completed, dueDate: dueDate, creationDate: creationDate) }
}

We see a todo item that will be created with some text given by the user and some default values. All properties are immutable. To create new objects with changed values, you have to use the setter — which in contrary to “normal” OOP doesn’t change a value in an existing object, but returns a new object reflecting the changes. If you wanted to create an item that has an due date and is completed, you would do something like:

let t = TodoItem(text: "Buy Beer").set(dueDate: Date.tomorrow.noon).set(completed: true)

There is one thing I don’t like much: Depending on the data provided by the user, a different method has to be called, pushing a lot of responsibility into the ui layer.

Instead I want to propose this form:

struct TodoItem: Equatable, Hashable {

    enum Change {
        case text(String)
        case completed(Bool)
        case id(UUID)
        case due(Date?)
    }

    let text: String
    let completed: Bool
    let id: UUID
    let dueDate: Date?
    let alterDate: Date?
    let creationDate: Date

    init(text: String) {
        self.init(text, false, UUID(), nil, Date(), nil)
    }

    func alter(_ changes: Change...) -> TodoItem { changes.reduce(self) { $0.alter($1) } }

// MARK: - private

    private init (_ text: String, _ completed: Bool, _ id: UUID, _ dueDate:Date?, _ creationDate: Date, _ alterDate:Date?) {
        self.text = text
        self.completed = completed
        self.id = id
        self.creationDate = creationDate
        self.dueDate = dueDate
        self.alterDate = alterDate
    }

    private func alter(_ change:Change) -> TodoItem {
        let alterDate = Date()
        switch change {
        case .text     (let text     ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
        case .completed(let completed): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
        case .id       (let id       ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
        case .due      (let dueDate  ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate )
        }
    }
}

Here we don’t use a setter for each value, but a method “alter” that takes any number of Change request, which are enums with associated values.

let t = TodoItem(text: "Buy Beer").set(dueDate: Date.tomorrow.noon).set(completed: true)

would become

let t = TodoItem(text: "Buy Beer").alter(.due(Date.tomorrow.noon), .completed(true))

This has one issue, though: to call alter(_ changes: Change...) -> TodoItem we must manage the user selection for certain values till everything is in place and alter(_ changes: ) can be called. Wouldn’t it be nice if we just could store all values in an array and send that list to alter(_ changes: )? Sure it would!

We change

func alter(_ changes: Change...) -> TodoItem { alter(changes) }
func alter(_ changes: [Change] ) -> TodoItem { changes.reduce(self) { $0.alter($1) } }

Now we can also do:

let t = TodoItem(text: "Buy Beer").alter([.due(Date.tomorrow.noon), .completed(true)])
Now we can collect any user input encoded as Change in an array and pass that to alter(_ changes: ).

Journaling

But let’s take it a bit further: let’s add journaling — which is rather simple as we just need to keep track of the intermediate items that will be created for every changed value.

As now TodoItems will refer to each other, we need to wrap the previous TodoItem, because: reasons. A simple array will do.

private let _previous: [TodoItem]
var previous: TodoItem? { _previous.first }

We change the private init and alter(_ change: ) to:

private init (_ text: String, _ completed: Bool, _ id: UUID, _ dueDate:Date?, _ creationDate: Date, _ alterDate:Date?, _ previous: TodoItem?) {
    // ..
    self._previous = previous != nil ? [previous!] : []
}
private func alter(_ change:Change) -> TodoItem {
    let alterDate = Date()
    switch change {
    case .text     (let text     ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate, self)
    case .completed(let completed): return TodoItem( text, completed, id, dueDate, creationDate, alterDate, self)
    case .id       (let id       ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate, self)
    case .due      (let dueDate  ): return TodoItem( text, completed, id, dueDate, creationDate, alterDate, self)
    }
}

let’s try it:

let dd = Date.tomorrow.noon

var todo = TodoItem(text: "Buy Beer").alter(.due(dd), .text("Buy Beer!!"))
todo = todo.alter(.due(dd.dayAfter))
todo = todo.alter(.completed(true))
for a in todo.history {
    print(a)
}

Result:

Buy Beer!!		completed:YES		creationdate:2020-05-01 06:16:35.9630		duedate:2020-05-03 12:00:00.0000		alterdate:2020-05-01 06:16:36.0050
Buy Beer!!		completed:NO		creationdate:2020-05-01 06:16:35.9630		duedate:2020-05-03 12:00:00.0000		alterdate:2020-05-01 06:16:36.0040
Buy Beer!!		completed:NO		creationdate:2020-05-01 06:16:35.9630		duedate:2020-05-02 12:00:00.0000		alterdate:2020-05-01 06:16:35.9900
Buy Beer		completed:NO		creationdate:2020-05-01 06:16:35.9630		duedate:2020-05-02 12:00:00.0000		alterdate:2020-05-01 06:16:35.9630
Buy Beer		completed:NO		creationdate:2020-05-01 06:16:35.9630		duedate:No due date						alterdate:No altered

It shows us the history from the most current version to the earliest version of the item.

But I don’t like the idea of having meta information, like previous and alterDate at the same level as the business information. let’s move those alterations into a child object Alteration:

struct TodoItem: Equatable, Hashable, CustomDebugStringConvertible {

    enum Change {
        case text(String)
        case completed(Bool)
        case id(UUID)
        case due(Date?)
    }

    private struct Alteration: Equatable, Hashable {
        init(_ date: Date, _ id: Int, _ previous: TodoItem?) {
            self.date = date
            self.id = id
            self._previous = previous != nil ? [previous!] : []
        }
        let date: Date
        let id: Int
        var previous: TodoItem? { _previous?.first }
        private let _previous: [TodoItem]?
    }

    let text: String
    let completed: Bool
    let id: UUID
    let dueDate: Date?
    var creationDate: Date { alteration.previous?.creationDate ?? alteration.date }
    private let alteration: Alteration

    init(text: String) {
        self.init(text, false, UUID(), nil, Alteration(Date(), 0, nil))
    }

    init(text: String, _ changes:  Change...) { self.init(text: text, changes) }
    init(text: String, _ changes: [Change]  ) {
        let               intermediate = TodoItem(text:text).alter(changes)
        self.text       = intermediate.text
        self.completed  = intermediate.completed
        self.id         = intermediate.id
        self.dueDate    = intermediate.dueDate
        self.alteration = Alteration(intermediate.creationDate, 0, nil)
    }

    func alter(_ changes: Change...) -> TodoItem { alter(changes) }
    func alter(_ changes: [Change] ) -> TodoItem { changes.reduce(self) { $0.alter($1, alteration.id + 1) } }

    var history: [TodoItem] { [self] + (alteration.previous != nil ? alteration.previous!.history : []) }

    var debugDescription: String {
        [
            ("completed"   , completed ? "YES" : "NO"),
            ("creationdate", df.string(from: creationDate)),
            ("duedate"     , dueDate != nil    ? df.string(from:   dueDate!)      : "No due date\t\t\t\t"),
            ("alterdate"   , alteration.id > 0 ? df.string(from: alteration.date) : "not altered")
        ].reduce(text) {
            $0 + "\t\t\($1.0):\($1.1)"
        }
    }

    // MARK: - private

    private init (_ text: String, _ completed: Bool, _ id: UUID, _ dueDate:Date?, _ alteration: Alteration) {
        self.text = text
        self.completed = completed
        self.id = id
        self.dueDate = dueDate
        self.alteration = alteration
    }

    private func alter(_ change:Change, _ alterID: Int) -> TodoItem {
        let alterDate = Date()
        switch change {
        case .text     (let text     ): return TodoItem( text, completed, id, dueDate, Alteration(alterDate, alterID, self) )
        case .completed(let completed): return TodoItem( text, completed, id, dueDate, Alteration(alterDate, alterID, self) )
        case .id       (let id       ): return TodoItem( text, completed, id, dueDate, Alteration(alterDate, alterID, self) )
        case .due      (let dueDate  ): return TodoItem( text, completed, id, dueDate, Alteration(alterDate, alterID, self) )
        }
    }
}

Note, that I also added to public inits init(text: String, _ changes: Change...) & init(text: String, _ changes: [Change]), which will use an intermediate item to create a item with those values but discarding the history — I guess this is as close as we can get with this approach to get an interface familiar with what we are used in OOP.

let t = TodoItem(text: "Buy beer", .due(dd), .text("Buy beer!!"), .due(dd.dayAfter), .completed(true))
t will not have any history bound to it. Besides that, it is equivalent to:
var todo = TodoItem(text: "Buy beer")
todo = todo.alter([.due(dd), .text("Buy beer!!")])
todo = todo.alter(.due(dd.dayAfter))
todo = todo.alter(.completed(true))

Also note, that Alteration keeps an id. The idea here is that this allows you to undo stuff, as we know that alterations with the same id on the same item where committed by the user at the same time.

Discussion

Please feel free to discuss this article.