OPEN SOURCE CODE

Introducing Swift-RunOnce

A declarative “one-time” code utility in Swift

Daniel Roth
Thumbtack Engineering

--

Photo by Brendan Church on Unsplash

The complete source code for Swift-RunOnce is freely available on Github under the Apache 2.0 license.

A few weeks ago, the iOS team at Thumbtack created a utility that enables a developer to declaratively define “one-time” code—code that should execute at most once for the lifetime of an object. The utility is both thread-safe and reentrant, and completely hides away the has/hasn’t run state of the closure, eliminating the possibility of invalidating it. We found this utility to be so useful that we released it as a small open source utility, which you can find on Github.

Motivation

The use of “One-time” code is a surprisingly common pattern in Swift apps. As an example, consider the case of tracking a “view” event on a UIViewController instance. This can easily be done by firing the tracking event within the viewDidAppear(animated:) function, which is called when the controller’s view is presented onto the screen. But, viewDidAppear(animated:) isn’t just called the first time the view is presented; it’s called every time. If the view reappears due to back navigation from a child view controller, the tracking event will fire again, corrupting the tracking data. If the developer only intends to track the initial view of the app, then she must ensure that the tracking code is only invoked once by wrapping the call with a check against some locally maintained variable.

This may not seem like a huge problem: I would venture a guess that most developers have already solved the problem of one-time code in their own apps by simply guarding the block with a boolean instance variable. This does work in simple cases, but it does not scale well to larger codebases for which the added complexity of having multiple engineers working in parallel against diverse product requirements increases the risk of making a critical mistake.

Note that Apple does provide a means to ensure that a closure is run only once for the lifetime of the app: dispatch_once. That is a slightly different functionality than I am discussing here (once for the life of an object). In the view tracking example, that would mean that we only ever track the appearance of a particular view once until the app restarts.

Status Quo: Flag and Check

The most common and intuitive way to create one-time code, what I am calling the “flag and check” model, is to simply check a boolean variable, which is set the first time the code is invoked. In the case of the viewController view tracking example, this would look something like:

Certainly, this works. But it has three significant shortcomings:

  • It is not defensive: The one-time logic to be executed and controlling boolean flag are tightly coupled in function, but they are not tightly coupled in the code. In fact, the declaration of the two components might be hundreds of lines apart in the source code. The only way someone would know that the two are linked would be by reading a comment, inferring it from the names of the objects, or relying on convention. Even if the flag is declared as private within the class, it is exposed to having other dependencies attached to it, which further muddies the link between the flag and the one-time code.
  • It is brittle: Even if the flag is private to the class, it can be modified outside of one-time code check, which would lead to a bug. Without compiler level enforcement that (1) the flag not be set the first time the one-time code runs, (2) the flag be set when the one-time code runs, and (3) the flag never be unset; the only thing keeping the flag state from being modified is an informal agreement among developers, hopefully documented in a comment.

    This may seem like a trivial concern in the viewDidAppear() example above, but consider what would happen if another developer attached an additional dependency to this flag—a second action that should occur the first time the view appears. Then later, due to a change in product specifications, the trigger logic for the second feature changes slightly. The engineer who implements this third change is unaware of the initial dependency on the flag, so they just update the flag state per the new requirements, breaking the initial constraint.

    Granted, this problem could be averted with multiple flags, better comments, etcetera. But all those solutions rely on agreements among people rather than enforceable constraints within the design of the system, which reduces their effectiveness considerably.
  • It’s verbose: A class can have many different blocks of one-time code that each need their own boolean flag. These additional flags distract from the critical code in the code base, increase cognitive load, and create additional surface area for bugs

These shortcomings may seem insignificant for the example given above, but they take on a much greater significance at scale. In an environment where many engineers work in parallel to advance separate product goals, relying on comments and variable names to preserve the integrity of the system is not only unrealistic, it’s irresponsible.

Ideal Solution: Requirements

There is clearly a lot of room for improvement over the status quo. For one, an ideal solution should only allow code declared as “one-time” to run at most once. Period. There should be no surface area by which someone may either preemptively set the state to “triggered”, nor reset the state and get the code to run the second time. Eliminating that surface area eliminates an entire category of potential bugs.

Second, an ideal solution would encapsulate both the guard/state and the one-time closure together. In fact, an ideal solution should completely internalize and hide away the state to protect it from errant modification.

Third, one-time code should be declared in exactly the same manner as any other code, just decorated with a one-time attribute. It should be able to capture state from its surroundings. One-time code should be declarable in-line and shouldn’t require the user to store or maintain additional state in, as an example, an instance variable. The utility should be declarative, not imperative.

Finally, if this is going to be used at scale across an entire project, it needs to be thread safe and reentrant. There is no way to guarantee that all usages will be on the main thread, nor one one-time code block won’t trigger another before it completes

In summary, the ideal requirements are:

  1. Cannot be invalidated
  2. Must internalize both the one-time code and the gating flag/logic
  3. Must be declarative, declarable in-line, and not require additional storage
  4. One-time code must be thread safe and reentrant

Alternatives to “Flag and Check”

In searching for a better way to handle this common pattern, we considered a number of potential improvements over the “flag and check” model described above, but ultimately felt that they all fell short of what we were looking for.

Settable only flag

A struct that behaves like a boolean but once set, cannot be unset. This addresses half of point 1 on our requirements list (it can’t be unset, but it could still be set too early). And while the example code is not written in a thread safe manner, with a little more work, it could be. But still doesn’t address points 2 and 3.

Encapsulating Struct

The SetOnlyFlag above can be extended slightly to satisfy requirement number 2 by including the one-time payload as a closure in the struct. Instead of storing a flag as an instance variable, we store this entire struct, and when we want to execute the code, we call run() on it.

This is an improvement, but it still cannot satisfy requirement number 3. The one-time payload needs to be provided to the struct at the time that the struct is initialized, but so too the struct needs to be initialized at the time the owning object is created. You could define a closure that takes arguments which you pass into the call to run(), but then the struct becomes less generic/reusable.

Lazy Var

A unique and creative solution to the problem, swift’s lazy var syntax can be leveraged to control execution of a block of code. Lazy vars are guaranteed (almost — lazy vars are not thread safe) by the language to only run once, so any code inside of them should only run once as well as well.

So let’s compare it to our requirements list:

  1. There is no boolean anymore, so it certainly can’t be reset—check
  2. The guard and logic are declared together — check
  3. The code isn’t inline, but we didn’t have to declare a separate object, so maybe give it half a check for creativity? 🤔—half check
  4. It turns out, lazy vars aren’t thread safe so you are still on your own to ensure that it only gets called by a single thread. That’s easier said than done… if two threads enter the initializer before a value is set, they will both run. If one sets the value before the other attempts to enter, the second one will be blocked. You can’t simply restrict the code to run on a particular thread, because if a different thread enters the initializer code first, the second one never can. To actually make this thread safe, you have to store another lock as an instance variable. Kind of makes you want to take away that half a check from before, right? 🤦🏻‍♂️—fail

Swift-RunOnce

The deeper one looks into this problem, the more apparent it becomes that there is no way to satisfy all four requirements simultaneously using the usual toolkit of classes, structs, functions, and clever application of abstraction. By definition, one-time code has state (whether or not is has run), and that state needs to be stored somewhere. The one-time payload can either be stored alongside that state, in which case we can’t declare it inline; or it can be declared inline, in which case it is no longer coupled with its state. And we need to be able to store some state for every instance of one-time code as each is distinct.

Storing the state

The objective-c runtime allows for any arbitrary object to be associated with another via objc_setAssociatedObject(_:_:_:_:) and objc_getAssociatedObject(_:_:). These powerful functions effectively enable us to create ad hoc instance variables, without having to actually declare them in our class or instantiate them at initialization. And by keeping the key on which we store the associations private, we can also ensure that they are immutable outside of the utility.

Mapping closures to state

We need to be able to associate a particular closure with a specific flag. This is a bit of a catch-22: An identifier that is created inline will be recreated each time the code is executed, resulting in a new, unique identifier. The only way to ensure that the identifier is consistent between calls would be to store it, but that’s precisely the thing we are trying to avoid doing in the first place.

Again, a little runtime hacking can solve the problem. All inline code has an invariant value for a given compilation—its location in the source code. We don’t typically think of the source code as being usable data within the running app, but there’s no reason it can’t be. If we can enforce that the code must be declared and executed inline, then we can use a combination of the code’s file, line, and column location to uniquely identify it. Swift provides three literal expressions that do just that: #fileId, #line, and #column. These literals are evaluated in place, but if you set them as default arguments to a function, they get evaluated in the context of the caller, not in the context of the function definition (another nice little hack). Great!

So all we have to do is declare a function that can be called with an object and a one-time closure (and some default arguments for the fileId and line and column numbers). When this function executes, it will check if a function with the given signature has already executed for the object. If it has, it exits early; if not, it updates its internal store and executes the one-time code. Since it is just a top-level function, there is no way to call it other than directly in code. The function contains no state and cannot be stored for execution elsewhere (technically, a top-level function can be assigned to a variable, but doing so invalidates the default arguments).

Actual usage

If we namespace this function in an enum called RunOnce we can call it like this:

Nice! But we can do even better. Most of the time, the object whose lifetime we care about is self. We can add this functionality to all NSObjects with a simple class extension, and since we now only have to pass a single closure into the function, we can utilize swift’s trailing closure syntax, which makes the function call look like the declarative wrapper we have been building toward all along:

Checking this solution against our original requirements:

  1. The solution is robust. The objective-c runtime hacks and the locking controls are all safely contained within the utility function. It is not possible for a user of this utility to mistakenly invalidate the state of a declared block of one-time code.
  2. The gating logic at one-time block are encapsulated together. Better yet, this solution completely separates the implementation of the utility from the application logic that it drives.
  3. Code using the utility is minimalistic and declarative. It is easy to parse the code and discover its intent.
  4. The code is both thread safe and reentrant.

We satisfied all four requirements! And more importantly, we replaced a pattern that shouldn’t fail with one that can’t fail, eliminating an entire category of potential bugs from our app.

The complete code is available on Github under the Apache 2.0 license. If you’re interested in coming up with creative solutions to problems like this one, we’d love to hear from you! You can find out about available positions on Thumbtack’s career page.

About Thumbtack

Thumbtack (www.thumbtack.com) is a local services marketplace where customers find and hire skilled professionals. Our app intelligently matches customers to electricians, landscapers, photographers and more with the right expertise, availability, and pricing. Headquartered in San Francisco, Thumbtack has raised more than $400 million from Baillie Gifford, Capital G, Javelin Venture Partners, Sequoia Capital, and Tiger Global Management among others.

--

--