Immutable Swift
Swift is a new language designed to work on a powerful, mature, established platform—a [new soul in a strange world][]. This causes tension in the design, as well as [some concern][Apple Developer Forums] due to the feeling that the power of Cocoa is inextricably tied to Objective-C’s dynamic nature.
I don’t have a strong opinion on that—my use of Cocoa generally eschews dynamic dispatch, and I like generics and type inference. Therefore I am fascinated by the possibilities Swift opens up: we are now able to try, in an idiomatic way, constructs that would have been strange and out-of-place in Objective-C. I think they’re exciting, and definitely worth exploring.
• • • • •
A truth that all programmers know: state management is why we get paid. Keeping track of state in an environment with multiple, multithreaded inputs and outputs, each prone to error and failure is like dancing ballet on a mined stage: a delicate, detailed, exacting task that invariably goes terribly wrong. We make our living minimizing how often that happens, and trying to figure out as quickly as possible why it did. (Cleanup duty is usually left to the database guys.)
Unsurprisingly, many smart programmers have [thought][stm] [deeply][reactive-cocoa] about ways to [mitigate][erlang-actors] this problem. Their solutions tend to boil down to formalizing two good practices:
- Use immutable data structures.
- Concentrate any changes where they can best be understood.
The first one is complicated in Objective-C. The available constructs are unwieldy, and they are never compiler-enforced; it’s hard to maintain the kind of discipline it takes to use them. Moreover, they are above and beyond the standard patterns, which makes them a part of the domain knowledge of the app, not the language. Not great.
The second practice could theoretically be done, but in reality ends up being completely ignored. Typically, the view controllers handle an unreasonable amount of application logic, instead of limiting themselves to mediating between a logic layer and the views.
Swift, however, has built-in immutability in the form of the let
keyword. By making judicious choices with our data, we can get much closer to these two ideas, in a way that is less onerous for us developers.
• • • • •
Did you know that in the US, adorable little girls sell Girl Scout cookies? They do. They visit your house hawking crack in a box and when they return you’re twice as fat and hate them and you still buy more. Just a bit of background for this app, which lets a girl see how many cookie boxes she’s sold and how many she had as her target.
Obviously, this app is trivial to write in any language. However, the architectural decisions can be widely different depending on what functionality the language provides. The Swift implementation I put together is available on [GitHub][]. Here’s the data model:
enum AppState {
case LoggedIn(SellerData)
case LoggedOut
}
struct SellerData {
let name:String
let salesTarget:Int
let salesToDate:Int
}
Simple, but already different from Objective-C. Everything in it is constant. Moreover, the SellerData
struct is only accessible via the AppState
enum, and only when the user is logged in. Immutability and state consistency are ensured at the data level.
To get a handle on changes, I followed the [Model-View-ViewModel] pattern, and introduced a ViewModel object that the controller can query for the current state and request state transitions from. This limits the controller to two roles:
- Requesting view-triggered updates to the state.
- Orchestrating the visual response to state changes.
In this case, the app only has two states: logged in, where it shows how the little girl is doing with her sales, and logged out, where it shows nothing. The transitions are triggered by tapping on the button. Here’s the logic:
class StatusViewController: UIViewController {
@IBOutlet var targetLabel: UILabel // other outlets omitted
var statusModel:StatusViewModel =
StatusViewModel(state:AppState.LoggedOut)
override func viewDidLoad() {
super.viewDidLoad()
self.targetLabel.text = self.statusModel.targetLabelText
}
@IBAction func toggleLoginState() {
switch self.statusModel.state {
case .LoggedOut:
self.statusModel = self.statusModel.login()
case .LoggedIn:
self.statusModel = self.statusModel.logout()
}
// Update things based on the new state.
self.targetLabel.text = self.statusModel.targetLabelText
}
}
I have more properties in the app, but the entirety of the logic is captured here: a state transition followed by a UI update. This means the entire state of the controller is changed out in that switch statement—no in-place updates of properties.
So how does StatusModelView
, the controller state object, work? Let’s look at the declaration and initializer:
struct StatusViewModel {
let state: AppState
let targetLabelText: String
let currentLabelText: String // other properties omitted
init(state:AppState) {
self.state = state
switch state {
case .LoggedOut:
self.targetLabelText = ""
self.currentLabelText = ""
case .LoggedIn(let sellerData):
self.targetLabelText = String(sellerData.salesTarget)
self.currentLabelText = String(sellerData.salesToDate)
}
}
}
Once again, all the properties are constants. So the state itself is constant and cannot be changed once instantiated. This pays off in spades in the initializer, because Swift has two requirements:
- Every constant in a struct must be initialized when the struct is initialized.
- In a switch statement, every case of an enum must be handled.
This means that if you write a custom initializer for your struct and initialize it with an enum, the compiler will force you to cover all the cases—if you miss a constant, you’ll be greeted with a build error. I’m a fan of compiler help, and this is about as helpful as it gets.
Finally, we have login()
and logout()
, the state transition methods:
func login() -> StatusViewModel {
switch state {
case .LoggedOut:
var stubData = SellerData(name:"Little Orphan Annie",
salesTarget: 100, salesToDate: 30)
return StatusViewModel(state:AppState.LoggedIn(stubData))
case .LoggedIn(let sellerdata):
return self
}
}
func logout() -> StatusViewModel {
switch state {
case .LoggedOut:
return self
case .LoggedIn:
return StatusViewModel(state:AppState.LoggedOut)
}
}
Since the StatusViewModel
is an immutable type, these functions create a new one if necessary—though if the app is already in the correct state, they return the current self
.
• • • • •
So there we have it. A very simple Cocoa app, but put together very differently from a normal Objective-C app. Even if you’ve used Model-View-ViewModel before in Cocoa, odds are you’ve never tried to use completely immutable data structures quite like this. I know I gave up very quickly the few times I considered it, because ensuring immutability actually increased the mental overhead of writing the Objective-C code.
That’s not to say that this approach is inherently better, or that the fact that Swift facilitates it is an unmitigated good thing. Yes, I can list benefits:
- The compiler verified that all properties were initialized.
- The
StatusViewModel
can be unit-tested with no dependencies on the UI. - Using an enum for the mutually exclusive states prevents access to data when the app is not in a state to provide it.
However, there are potential drawbacks, too:
- This data model is very simple. What happens if it’s deeply nested and the change in state is somewhere five levels deep? How do I execute those changes, let alone efficiently?
- There is exactly one UI transition, which can be handled in the same way no matter which way we are toggling. This won’t always be the case—what will the controller code end up looking like when the symmetry breaks?
- The app only needs to handle one event. Will the benefits of this approach remain in the face of the explosion of transitions as the app grows to handle more?
I don’t have answers yet, but since I’m trying to build a more complex app on these principles, I might soon.
Or I might not. Because that’s not really the point. The point is to explore. That’s the true beauty of this new language. Supported by a platform we know well, we are trying (and [smashing][smashing-swift]) things every day now: [core semantics][], [function composition][], [functional constructs][]—old concepts rejiggered and foreign ones reinvented. Some ideas are great, others less so, some will be popular and others forgotten, but Swift is three weeks old and it is unmistakably alive. And like all living things, it will grow—perhaps very much afield from where it started.
We may lose some things in this undiscovered country. But let’s not look behind too much—there is so much ahead.
[Model-View-ViewModel]: http://www.objc.io/issue-13/mvvm.html
[Apple Developer Forums]: https://devforums.apple.com/thread/231414
[CookieWars]: https://github.com/nomothetis/CookieWars
[stm]: http://clojure.org/rationale
[reactive-cocoa]: https://github.com/ReactiveCocoa/ReactiveCocoa
[erlang-actors]: http://savanne.be/articles/concurrency-in-erlang-scala/
[KPIs]: http://en.wikipedia.org/wiki/Key_performance_indicator
[function composition]: http://railsware.com/blog/2014/06/17/composing-functions-in-swift/
[functional constructs]: https://github.com/maxpow4h/swiftz
[smashing-swift]: http://nomothetis.svbtle.com/smashing-swift
[core semantics]: https://plus.google.com/+AndreyTarantsov/posts/AZmU5c3nJwc
[new soul in a strange world]: http://www.youtube.com/watch?v=g7pvy37XaRw
[GitHub]: https://github.com/nomothetis/CookieWars