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 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 deeply about ways to mitigate this problem. Their solutions tend to boil down to formalizing two good practices:

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.

logged-out.png

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:

  1. Requesting view-triggered updates to the state.
  2. 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:

  1. Every constant in a struct must be initialized when the struct is initialized.
  2. 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:

However, there are potential drawbacks, too:

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) 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.

 
299
Kudos
 
299
Kudos

Now read this

Error Handling in Swift: Might and Magic

If distance brings perspective, proximity brings understanding. Concepts that were remote and downright strange when I played with them in Haskell or read about them in Scala are blindingly obvious solutions to any number of problems now... Continue →