Bit masks as feature flags
Like many languages, Swift offers enumeration as first-class type. An enumeration defines a new group type of related values and allows us to work with those values in a type-safe way.
Among other things, enumerations are great to represent sets of options.
Let’s take as example
UIViewAnimationOptions. This type describes the available options to animate an
UIView. You can combine these options, sometimes it is even mandatory.
In Objective-C, this type is defined as an enumeration. To combine multiple options together, you need to “pipe” them, aka using a bitwise OR.
However, in Swift this type is defined and used differently: it is a
struct conforming to the
OptionSet protocol, and you use it like a set.
Enumerations have one problem, you can only set one option at the time. This is the soul of an enumeration. The Objective-C version of
UIViewAnimationOptions is expressed in a hacky-way, hijacking the prime goal of the enumeration.
OptionSet was designed to solve this very problem: a set where you can set multiple options at the same time. Under the hood, OptionSet is represented as a bit field but presented as an operation set. Basically, OptionSet enables us to represent bitset types and perform easy bit masks and bitwise operations.
Conforming to OptionSet only requires to provide a
rawValue property of integer type. This type will be used as the underlying storage for the bit field. Indeed, integers are stored as a series of bits in memory. The size of the integer type will determine the maximum number of options you can define for your set to work accurately.
Int8 and will be able to represent up to 8 options accurately.
Each option represents a single bit of the
rawValue. In order to represent these options correctly, we need to assign ascending powers of two to each option: 1, 2, 4, 8, 16, etc.
Now when combining two or more options (aka bit mask), there is no overlapping.
OptionSet conforms to
SetAlgebra, meaning you can manipulate it with multiple mathematical set operations: insert, remove, contains, intersection, etc.
Bitwise left shifting
Integers are stored as bitfield. For example, using
Int8, the value 6 (decimal) or 0x40 (hexadecimal) is stored like so:
A common bitwise operation is left shifting, noted
Left shifting shift the digits to the left according to the offset specified and fill the right empty spaces with zeros. Shifting this bit pattern to the left from one position (
6 << 1) result in the number 12 (decimal):
Left shifting is equivalent to multiplication by powers of 2, regarding the offset. Shifting 6 from three positions (
6 << 3) result in the number 48 ($6 \times 2^3$).
Using left shifting is pretty common good practice when describing
OptionSet options ; just increase the shifting position and let the math do the rest.
Let’s look at a real-world example. Feature flags is a technique allowing to modify system behavior without changing code. They can help us to deliver new functionality to users rapidly and safely.
For example, you could be in the process of rewriting a part of your app to improve its efficiency. This work will take some time, probably multiple weeks, but you don’t want to impact your team, that will continue to work on other parts of the app. Branching is a no go, thanks to previous experiences of merging long-lived branches. Instead, the people working on that rewrite will use a specific feature flag to use the new implementation, while the other will continue to use the current one as usual.
Canary deployment is another great benefit of feature flags. Say your rewrite is ready and you would like to test it in real conditions. However, you don’t want to deliver it to all of your users, and go back to the old implementation in case there is something wrong. With feature flags, you can only activate the new implementation for a small percentage of users.
Since WWDC 2017, Apple introduced “phased releases”, the ability to gradually release new versions of an application. However, with your own implementation of feature flags you get fine grained control over who is exposed to which feature and when. This is also useful when rolling out time based functionalities and need absolute control.
Feature flags can be implemented in many ways, but all of them will introduce additional complexity in your system. Our goal is to constrain this complexity by using a smart implementation.
Let’s see how
OptionSet can help us reduce this complexity.
Fundamentally, that’s all what we need.
Now, let’s say we want a particular combination of these flags, a feature groups. We can use the array notation like
UIView.AnimationOptions or we can take advantage of the capabilities offer by
OptionSet. We can add the following to our
FeatureFlags type, and use it like any other option.
Usually, you retrieve these flags from an API, where each flag is represented by a boolean value. This is where the magic of
OptionSet begin: instead of list of boolean flags, you can use a single integer value representing all your flags!
options now contains
feature7 ($1+32+64 = 97$).
You even can have several
FeatureFlags: a global one, one for each of your key functionalities, one specific to your user, etc. And of course, combine them!
One More Thing
OptionSet isn’t a collection. You can’t count them or iterate over them. However, since we only define them with integer values, we can improve the protocol to help us work with them.
all produces an instance of your type with all options, while
members computes the list of all options of a particular instance, practical to iterate.
Let’s update the conformance and add the new property to our type. We also add conformance to
CustomStringConvertible for debug purpose.
Let’s try with a set of examples.
Last updated on 15th September 2019