Alexis King’s post “Parse, don’t validate” is the best thing I’ve read in months. And not just for its pitch-perfect use of “cromulent”. Please find some time and commit to reading it all — it will make you a better programmer — but here’s a small taste:
The problem is that validation-based approaches make it extremely difficult or impossible to determine if everything was actually validated up front… The entire program must assume that raising an exception anywhere is not only possible, it’s regularly necessary.
Parsing avoids this problem by stratifying the program into two phases — parsing and execution — where failure due to invalid input can only happen in the first phase. The set of remaining failure modes during execution is minimal by comparison, and they can be handled with the tender care they require.
It sparks in me some new thoughts with regard to my favorite Ben Sandofsky quote:
If the boilerplate gets mind numbing, refactor it into class methods. If that gets to be too much, build some model objects. The goal is to iterate toward your domain model. There’s a reason Apple’s frameworks only get you 80% of the way there. That remaining 20% is called ‘Your App.’
King’s post makes concrete for me how futile it is to pass and occasionally validate data in pursuit of this goal. It’s only through the successive parsing of our data that we iterate towards our domain model. Validating just rearranges deck chairs on the Titanics of our ever-growing codebases.
One of the first things presented about SwiftUI is that its views determine their own sizes, and that those sizes are fixed … This statement fundamentally changes everything you know about layout. Until you understand all of the repercussions of it, you’ll be constantly feeling like you’re fighting SwiftUI for even the simplest layouts.
–Scott James Remnant, netsplit.com
This is definitly my experience with layout in SwiftUI. It took this masterful series of posts by Scott James Remnant (yes, that Scott James Remnant) to set me straight. I highly recommend starting with “Views Have Fixed Sizes” and continuing to click “Next” until empty.
One of the venerable Foundation
APIs to get a Combine
extension in Catalina/iOS 13 is the Timer
class:
static func publish(
every interval: TimeInterval,
tolerance: TimeInterval? = nil,
on runLoop: RunLoop,
in mode: RunLoop.Mode,
options: RunLoop.SchedulerOptions? = nil)
-> Timer.TimerPublisher
In practice we’d probably use it something like:
import Foundation
import Combine
let subscription =
Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.sink { _ in
print("timer fired")
}
// Time passes...
subscription.cancel()
Note that Timer.publish
returns a ConnectablePublisher
meaning we need to call connect
or use autoconnect
to start it publishing. Otherwise this feels like a pretty straight-forward definition of a timer.
So straight-forward, in fact, that it looks pretty familiar. Here’s how we might have written a timer before Combine
:
let timer =
Timer(timeInterval: 1.0, repeats: true) { _ in
print("timer fired")
}
RunLoop.main.add(timer, forMode: .default)
// Time passes...
timer.invalidate()
We explicitly add the timer to the run loop instead of using autoconnect
to start it. And to stop it we call invalidate
on the returned timer
ref rather than cancel
on an AnyCancellable
. But all the same pieces are there. The interval, run loop, closure to execute, etc.
So it’s tempting to say that, if we’re targeting Catalina/iOS 13 but not interested in filtering timer events through a bunch of Combine operators, there’s really no benefit to using Timer
’s publisher API.
But this overlooks a major yet somewhat invisible benefit, which is life cycle management.
Let’s say we have a class that uses an old-school timer:
class OldTimer {
let timer: Timer
init() {
timer =
Timer(timeInterval: 1.0, repeats: true) { _ in
print("Old Timer, go!")
}
RunLoop.main.add(timer, forMode: .default)
}
}
var old: OldTimer? = OldTimer()
// ⏱ Wait two seconds ⏱
old = nil
What do we we see in our output?
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
Old Timer, go!
…
Uh oh! We forgot some crucial cleanup in our deinit
:
class OldTimer {
let timer: Timer
// ...
deinit {
timer.invalidate()
}
}
Now we get the expected results:
Old Timer, go!
Old Timer, go!
But how easy is it to forget that deinit
? I do it practically every time I use a Timer
, and often in less obvious circumstances than what’s presented here.
Let’s look at the publisher case:
class NewSchool {
let subscription: AnyCancellable
init() {
subscription =
Timer.publish(every:1.0, on:.main, in:.default)
.autoconnect()
.sink { _ in
print("New School, I choose you!")
}
}
deinit {
subscription.cancel()
}
}
var new: NewSchool? = NewSchool()
// ⏱ Two seconds later ⏱
new = nil
We see:
New School, I choose you!
New School, I choose you!
Not a big difference, right? Here’s the trick, though: unlike a Timer
which lives on irrespective of our reference to it, the life cycle of a Subscription
is completeley tied up in its value. If it gets set to nil
and/or released, it automatically cancels its connection to its publisher.
So in our example, because the default behavior of properties like subscription
is to be retained while their object exists and automatically nil
’d out when it’s deallocated, we don’t need an explicit dealloc
statement at all! In other words:
class NewSchool {
let subscription: AnyCancellable
init() {
subscription =
Timer.publish(every:1.0, on:.main, in:.default)
.autoconnect()
.sink { _ in
print("New School, I choose you!")
}
}
}
var new: NewSchool? = NewSchool()
// ⏱ Two seconds later ⏱
new = nil
still does exactly what we want it to:
New School, I choose you!
New School, I choose you!
So publishers and subscriptions present a much more intuitive way to think about cancellation, invalidation, and life cycles. And that’s great! But it’s also worth pointing out they make the default, naïve implementation the correct one. This makes them effective protection against bugs of omission — the value of which shouldn’t be underestimated.
I got to write a post over at NSHipster (😮) this week about the past, present, and glorious future of error handling in Swift and how Result
fits in with it all. Let me know what you think!
One thing I really love about Swift is its exhaustive switch
statement. As programmers, we live with a creeping dread of change and the unforeseen consequences it can wreck on our carefully calibrated computations. And switch
is an excellent hammer with which we can nail down at least one future loose end.
If we have code that calculates how many spaces a character can move in a game, for example:
enum Character {
case elf, dwarf, human
}
func spaces(for character: Character) -> Int {
switch character {
case .elf:
return 7 // Spritely!
case .dwarf:
return 3 // Short legs; big heart.
case .human:
return 5 // Even Steven.
}
}
…and we later add a dragon:
enum Character {
case elf, dwarf, human, dragon
}
No problem! The Swift compiler let’s us know there’s a spot where we haven’t considered what to do with dragons, and won’t let us continue until we deal with it:
func spaces(for character: Character) -> Int {
switch character { 🛑 Switch must be exhaustive
//...
}
}
This future-proofing is such a comfort to me, I cram everything I can in switch
es. But sometimes it’s just not feasible. Let’s look at a simple person model. It can hold a firstName
and/or a lastName
, and put those together in a description
:
class Person: CustomStringConvertible {
var firstName: String?
var lastName: String?
var description: String {
return [firstName, lastName]
.compactMap { $0 }
.joined(separator: " ")
}
}
It also has utility methods for letting us know if it has a full name, and for returning either the first or last name, whichever is present, for more informal modes of address:
extension Person {
var isFullName: Bool {
guard
firstName != nil,
lastName != nil else {
return false
}
return true
}
var firstOrLast: String? {
return firstName ?? lastName
}
}
This all looks fairly straightforward, but I’m terrified. What if later we add a middleName
property? Nothing will break, yet everything will be wrong. It will be up to whomever implements middleName
to search for all the places it might be relevant1 and incorporate it.
How can we get switch
-like exhaustiveness when dealing with the properties of a type? The first step (and our first clue) is to treat the properties as a set. What if we put them in a tuple?
class Person: CustomStringConvertible {
var name: (first: String?, last: String?)
}
Accessing the properties of Person
is still painless:2
let myPerson = Person()
myPerson.name.first = "Deedlit"
And we can now rewrite description
and isFullName
in terms of the tuple:3
var description: String {
let (first, last) = name
return [first, last]
.compactMap { $0 }
.joined(separator: " ")
}
var isFullName: Bool {
guard case (_?, _?) = name else {
return false
}
return true
}
But firstOrLast
, which only depends on a first and last name and doesn’t care if we ever add more, can reference the values of the tuple directly:
var firstOrLast: String? {
return name.first ?? name.last
}
Now what happens if we add a middle name?
class Person: CustomStringConvertible {
var name: (
first: String?,
middle: String?,
last: String?
)
var description: String {
let (first, last) = name
🛑 tuples have a different number of elements
// ...
}
}
extension Person {
var isFullName: Bool {
guard case (_?, _?) = name else {
🛑 tuples have a different number of elements
}
// ...
}
var firstOrLast: String? {
// ...
}
}
description
and isFullName
are both flagged with compiler warnings, firstOrLast
just keeps on trucking, and I cut my antacid budget by 4×.
So, to be clear, habitually shoving all properties into a tuple won’t scale well. But it is a useful tactic to employ when dealing with models, mocks or anything that has:
When we are confronted with such beasts, we can save our future-selves some consternation by popping properties into tuples.
1: Think of all the places extension Person
could live if you really want to break out in a cold sweat.↩︎
2: Though, outside the forced reality of a blog example, the Person
model should be a struct
, its name
should be a let
, and everything immutable. Still, it’s good to know mutable tuples are a thing. ↩︎
3: The (_?, _?)
in isFullName
might look odd. It is a combination of the Wildcard Pattern and the Optional Pattern. It means, roughly, “a tuple of two non-optional things.” We’ve talked about the Optional Pattern before, over here.↩︎
subscribe via RSS