Computed properties in Swift
Basics article available: PropertiesA major part of what makes Swift such a powerful and versatile language is the fact that we usually have multiple options at our disposal when it comes to picking what language features to use when forming the solution to a given problem. However, that versatility can also be a source of confusion and debate, especially when there’s not a clear-cut line between the key use cases of the features that we’re considering.
This week, let’s take a look at one such language feature — computed properties — and how they can let us build really elegant convenience APIs, how to avoid accidentally hiding performance problems when deploying them, and a few different strategies for picking between a computed property and a method.
Properties are for data
Whether a property is computed or stored should ideally be just an implementation detail — especially since there’s no way of telling exactly how a property is stored by just looking at the code in which it’s being used. So just like how stored properties make up the data that a type is storing, computed properties can be seen as a way to compute a type’s data whenever it’s needed.
Let’s say that we’re working on an app for listening to podcasts, and that the state that a given podcast episode is in (whether it has been downloaded, listened to, etc.) is modeled using a State
enum that looks like this:
extension Episode {
enum State {
case awaitingDownload
case downloaded
case listening(progress: Double)
case finished
}
}
We then give our Episode
model a stored state
property that we can use to make decisions depending on a given episode’s state — for example to be able to show the user whether an episode has been downloaded or not. However, since that particular use case is so common within our code base, we wouldn’t want to have to manually switch on state
in many different places — so we also give Episode
a computed isDownloaded
property that we can reuse wherever needed:
extension Episode {
var isDownloaded: Bool {
switch state {
case .awaitingDownload:
return false
case .downloaded, .listening, .finished:
return true
}
}
}
The reason we switch on state
above, rather than using an if
or guard
statement, is to “force” ourselves to update this code if we ever add a new case to our State
enum — since otherwise we might end up handling that new case in an incorrect way without knowing about it.
The above implementation is arguably a great use case for a computed property — it eliminates boilerplate, adds convenience, and is acting exactly as if it was a read-only stored property — it’s all about giving us access to a particular part of a model’s data.
Accidental bottlenecks
Now let’s take a look at the flip side of the coin — how computed properties, although really convenient, can sometimes end up causing accidental performance bottlenecks if we’re not careful. Continuing with the above podcast app example, let’s say that the way we model a user’s library of podcast subscriptions is through a Library
struct, which also contains metadata like when the last server sync took place:
struct Library {
var lastSyncDate: Date
var downloadNewEpisodes: Bool
var podcasts: [Podcast]
}
While the above array of Podcast
models is all that we need to render most of the views within our app, we do have a few places in which we want to show all of a user’s podcasts as a flat list. Just like how we extended Episode
with an isDownloaded
property before, an initial idea might be to do the same here — to add a computed allEpisodes
property that gathers all episodes from each podcast within the user’s library — like this:
extension Library {
var allEpisodes: [Episode] {
return podcasts.flatMap { $0.episodes }
}
}
To learn more about flatMap
, check out the “Map, FlatMap and CompactMap” Basics article.
The above API might look really nice and simple — but it has a quite major flaw — its time complexity is linear (or O(n)
), since in order to compute our allEpisodes
property we need to iterate through all podcasts at once. That might not seem like a big deal at first — but could become really problematic in situations like this, when we’re accessing the above property every time that we’re dequeuing a cell within a UITableView
:
class AllEpisodesViewController: UITableViewController {
...
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
)
// Here we're accessing allEpisodes just as if it was a
// stored property, and there's no way of telling that this
// will actually cause an O(n) evaluation under the hood:
let episode = library.allEpisodes[indexPath.row]
cell.textLabel?.text = episode.title
cell.detailTextLabel?.text = episode.duration
return cell
}
}
Since table view cells can be dequeued at a really rapid pace — while the user is scrolling — the above code is almost guaranteed to become a performance bottleneck sooner or later, since our current allEpisodes
implementation will keep iterating through all podcasts every single time it’s accessed. Not great.
While gathering all the episodes from each podcast is inherently going to be an O(n)
operation with our current model structure, we could improve the way we’re signaling that complexity through our API. Rather than making allEpisodes
appear as just another property, let’s make it a method instead. That way, it will look more like an action that is being performed (which it is), rather than just a quick way of of accessing a piece of data:
extension Library {
func allEpisodes() -> [Episode] {
return podcasts.flatMap { $0.episodes }
}
}
If we also update our AllEpisodesViewController
to accept an array of episodes as part of its initializer, rather than accessing our Library
model directly, then we get the following call site — which looks much clearer than our previous implementation:
let vc = AllEpisodesViewController(episodes: library.allEpisodes())
Within our view controller, we can still keep accessing all of our episodes just like before — only now that array is just constructed once, rather than every time a cell is dequeued, which is a big win.
Conveniently lazy
Turning any computed property that can’t be executed in constant time into a method instead usually improves the overall clarity of our APIs — since we’re now strongly signaling that there’s some form of cost associated with accessing them. But in doing so, we’ve also lost a bit of the “elegance” that using properties gave us.
However, in many of these situations, there’s actually a way to achieve clarity, elegance and performance — all at the same time. To be able to keep using a property, without having to do all of the processing work up-front — by using lazy evaluation.
Like we took a look at in “Swift sequences: The art of being lazy” and “String parsing in Swift”, postponing iterating over a sequence until it’s actually needed can give us a substantial boost in performance — so let’s take a look at how we could use that technique to be able to turn allEpisodes
back into a property.
We’ll start by extending our Library
model with two new types — one for our sequence of episodes, and one for iterating over the elements within that sequence:
extension Library {
struct AllEpisodesSequence {
fileprivate let library: Library
}
struct AllEpisodesIterator {
private let library: Library
private var podcastIndex = 0
private var episodeIndex = 0
fileprivate init(library: Library) {
self.library = library
}
}
}
To turn AllEpisodesSequence
into a first class Swift sequence, all we have to do is to make it conform to Sequence
, by implementing the makeIterator
factory method:
extension Library.AllEpisodesSequence: Sequence {
func makeIterator() -> Library.AllEpisodesIterator {
return Library.AllEpisodesIterator(library: library)
}
}
Next, let’s make our iterator conform to the required IteratorProtocol
, and implement our actual iteration code. We’ll do that by reading each episode within a podcast, and when there are no more episodes to be found, we move on to the next podcast — until all episodes have been returned, like this:
extension Library.AllEpisodesIterator: IteratorProtocol {
mutating func next() -> Episode? {
guard podcastIndex < library.podcasts.count else {
return nil
}
let podcast = library.podcasts[podcastIndex]
guard episodeIndex < podcast.episodes.count else {
episodeIndex = 0
podcastIndex += 1
return next()
}
let episode = podcast.episodes[episodeIndex]
episodeIndex += 1
return episode
}
}
With the above in place, we’re now free to turn allEpisodes
back into a computed property — since it no longer requires any up-front evaluation, and simply returns a new AllEpisodesSequence
instance in constant time:
extension Library {
var allEpisodes: AllEpisodesSequence {
return AllEpisodesSequence(library: self)
}
}
While the above approach requires more code than what we had before, there are a few key benefits to it. The first is that it’s now completely impossible to simply subscript into the sequence that allEpisodes
returns, since Sequence
doesn’t imply random access to any underlying element:
// Compiler error: Library.AllEpisodesSequence has no subscripts
let episode = library.allEpisodes[indexPath.row]
That might not seem like a benefit at first, but it prevents us from accidentally causing the kind of performance bottlenecks that we faced before — by forcing us to copy our allEpisodes
sequence into an Array
before we’ll be able to gain random access to the episodes within it:
let episodes = Array(library.allEpisodes)
let vc = AllEpisodesViewController(episodes: episodes)
While there’s really nothing stopping us from performing the above array conversion each time we’d want to read a single episode — it’d be a much more deliberate choice than when we were accidentally subscripting into an array that looked like it was stored, rather than computed.
Another benefit is that we no longer need to unnecessarily gather all episodes from every podcast, if all we’re looking for is a small subset. For example, if we only wanted to show the user their next upcoming episode — we could now simply do this:
let nextEpisode = library.allEpisodes.first
The beauty of using lazy evaluation is that, even though allEpisodes
returns a sequence, the above operation has constant time complexity — just as you’d expect from accessing first
on any other sequence. Pretty great!
It’s all about the semantics
Now that we’re able to turn even complex operations into computed properties, without any up-front evaluation, the big question is — what are the remaining use cases for argument-less methods?
The answer depends very much on what kind of semantics that we want a given API to have. A property very much implies some form of access to the current state of a value or object — without changing it. So anything that modifies state, for example by returning a new value, is most likely better represented by a method — like this one, which updates the state
of one of our Episode
models from before:
extension Episode {
func finished() -> Episode {
var episode = self
episode.state = .finished
return episode
}
}
Comparing the above API to what it’d look like if were using a property instead — it’s quite clear that a method gives us just the right semantics for this kind of situation:
// Looks like we're performing an action to finish the episode:
let finishedEpisode = episode.finished()
// Looks like we're accessing some form of "finished" data:
let finishedEpisode = episode.finished
Much of the same logic can be applied to static APIs as well, but we might choose to make certain exceptions, especially if we’re optimizing our APIs to be called using dot syntax. For a few examples of designing such static APIs, see “Static factory methods in Swift” and “Rule-based logic in Swift”.
Conclusion
Computed properties are incredibly useful — and can enable us to design simpler, more lightweight APIs. However, it’s important to make sure that such simplicity isn’t only perceived, but also reflected in the underlying implementation as well. Otherwise we risk hiding performance bottlenecks, and in those situations it’s often better to either opt for a method instead — or deploy lazy evaluation, if appropriate.
What do you think? How do you decide between a computed property and a method, and do you think this article will help you make such decisions more easily in the future? Let me know — along with your questions, comments and feedback — either via email or Twitter.
Thanks for reading! 🚀