Interior Mutability in Swift
I'm working on a project at the moment where I need to start some task as an "anonymous" user (i.e. somebody without an account who's just downloaded the app) and then allow them to authenticate, and complete the task as an authenticated user, remembering all the actions they took before, but now with additional privileges.
These anonymous users are actually authenticated behind the scenes with some anonymous user ID, allowing conversions can be more accurately tracked. There is also other benefits to this approach, such as making sure the API is not publically exposed to anyone the internet. Therefore, we have the concept of a user of the app.
protocol User {
var uid: String { get }
var apiClient: APIClient { get }
}
For example, the implementation of an anonymous user could take a simple form which is essentially is just a wrapper to access the API.
class AnonymousUser: User {
let uid: String
let apiClient: APIClient
// initialised once a uid is known from some authenticating service
init(uid: String) {
self.uid = uid
self.apiClient = APIClient(uid: uid)
}
}
Authenticated users take a very similar form, maybe with some additional state contained within it, for example.
class AuthenticatedUser: User {
let uid: String
let apiClient: APIClient
// initialised once a uid is known from some authenticating service
init(uid: String) {
self.uid = uid
self.apiClient = APIClient(uid: uid)
}
// other properties, unique to authenticated users...
var friends = 0
var enemies = 0
}
Now, we need to have some idea of a session that can keep track of the user and the tasks they're performing.
class Session {
var user: User
private let sessionHelper: SessionHelper
init(user: User) {
self.user = user
self.sessionHelper = SessionHelper(user: user)
}
// user session....
var likesPuppies = false
var firstName = "John"
}
// in the startup of the app...
let uid = AuthService.getAnonymousUserId()
let anonymousUser = AnonymousUser(uid: uid)
let userInfoSession = Session(user: anonymousUser)
// use the session!
Right, so we have our users and our session.
Now, when the anonymous user becomes authenticated we can just fetch their new authenticated user id, create an AuthenticatedUser
object and reassign the session's user
.
Not so fast.
If nested class instances also refer to the User
, they will have their own references to the user object, which will not be automatically updated when we reassign the user
.
For example, you may have noticed the private SessionHelper
above.
It contains logic and helpers for the session, internal to the session and unimportant for a consumer of this API (don't worry about the details).
Importantly, it also refers to the same User
instance as was assigned to the session.
We want a way to ensure that this user is updated for anyone that refered to the original instance.
let newUid = AuthService.getAuthenticatedUserId()
let authenticatedUser = AuthenticatedUser(uid: newUid)
userInfoSession.user = authenticatedUser
// 😬 OOPS!
// the `sessionHelper` was not updated
// it still refers to the old, anonymous user!
We could work around this by adding a property observer to the Session
's user
property,
but that requires us to keep it updated if other properties come along in the future, which harms maintainability.
// inside 'Session'
var user: User {
didSet {
sessionHelper.user = user
}
}
There's other issues with this approach, for example, it won't work if the SessionHelper
has an immutable reference to the user
or any other dependency in the future takes an immutable reference.
Additionally, if the SessionHelper
itself also has internal other references to the User
, there will need to be additional logic to update those as well.
A possible solution is interior mutability, which is the concept of being able to modify internal values, even when an immutable (let
) reference is held to a given value.
It allows us to pass a given reference around to many consumers, then update the underlying value without affecting the original reference.
This means that all consumers get the newly modified value for free, no manual updating required.
We can make use of this pattern in Swift by simply using a reference type (class
).
Classes are passed by reference, so even if we hold an immutable reference to a class, we can still update the inner values while maintaining the original reference.
class UserHolder {
var user: User
init(_ user: User) {
self.user = user
}
}
Now instead of passing a User
, we pass a UserHolder
instead.
final class Session {
var user: UserHolder
private let sessionHelper: SessionHelper
init(user: UserHolder) {
self.user = user
self.sessionHelper = SessionHelper(user: user)
}
// user session....
var likesPuppies = false
var firstName = "John"
}
// in the startup of the app...
let uid = AuthService.getAnonymousUserId()
let anonymousUser = AnonymousUser(uid: uid)
let heldUser = UserHolder(anonymousUser)
let userInfoSession = Session(user: heldUser)
// use the session!
Now, when the user is updated, we just need to update the user
property of UserHolder
and all places that refer to the UserHolder
will see the updated user
right away!
let newUid = AuthService.getAuthenticatedUserId()
let authenticatedUser = AuthenticatedUser(uid: newUid)
// update the inner value of the UserHolder
userInfoSession.user.user = authenticatedUser
// YES! we know it's updated everywhere that takes a UserHolder
This is a great win for reducing verbosity, but there's now an additional layer of indirection between the UserHolder
and the User
.
Thankfully, Swift has a feature called dynamic member lookup, allowing a base type to look up arbitrary properties at runtime.
We can make use of this feature, along with generics, to arrive at a generalised "Box" that can contain another type and provide interior mutability.
@dynamicMemberLookup
class Box<T> {
var value: T
init(_ value: T) {
self.value = value
}
subscript<R>(dynamicMember keyPath: KeyPath<T, R>) -> R {
value[keyPath: keyPath]
}
}
Now using Box<User>
in place of UserHolder
above more clearly describes the semantics of this container type and allows it to be re-used among any type.
We can also directly access the properties of the inner type, thanks to @dynamicMemberLookup
.
How cool is that!