-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathhow-optionset-works-inside-the-swift-compiler.html
345 lines (296 loc) · 21.1 KB
/
how-optionset-works-inside-the-swift-compiler.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="Swift's OptionSet protocol is the bridged version of Objective-C's NS_OPTIONS enum -- a handy tool that allows you to very efficiently define a combination of "options" that can be used for a specific operation. Let's see what this protocol is, why OptionSets are preferred over a regular Set in some cases and finally what compiler magics it possesses.">
<meta name="title" content="How OptionSet works inside the Swift Compiler">
<meta name="url" content="https://swiftrocks.com/how-optionset-works-inside-the-swift-compiler">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="How OptionSet works inside the Swift Compiler"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="Swift's OptionSet protocol is the bridged version of Objective-C's NS_OPTIONS enum -- a handy tool that allows you to very efficiently define a combination of "options" that can be used for a specific operation. Let's see what this protocol is, why OptionSets are preferred over a regular Set in some cases and finally what compiler magics it possesses."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/how-optionset-works-inside-the-swift-compiler"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="How OptionSet works inside the Swift Compiler"/>
<meta name="twitter:description" content="Swift's OptionSet protocol is the bridged version of Objective-C's NS_OPTIONS enum -- a handy tool that allows you to very efficiently define a combination of "options" that can be used for a specific operation. Let's see what this protocol is, why OptionSets are preferred over a regular Set in some cases and finally what compiler magics it possesses."/>
<meta name="twitter:site" content="@rockbruno_"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="images/favicon/iconsmall2.png" sizes="32x32" />
<link rel="apple-touch-icon" href="images/favicon/iconsmall2.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism4.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style48.css">
<link rel="stylesheet" type="text/css" href="css/sponsor4.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/how-optionset-works-inside-the-swift-compiler"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2019-12-03T06:00:00+00:00",
"dateModified": "2020-04-12T14:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb.jpg"
}
},
"headline": "How OptionSet works inside the Swift Compiler",
"abstract": "Swift's OptionSet protocol is the bridged version of Objective-C's NS_OPTIONS enum -- a handy tool that allows you to very efficiently define a combination of 'options' that can be used for a specific operation. Let's see what this protocol is, why OptionSets are preferred over a regular Set in some cases and finally what compiler magics it possesses."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2light.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=How OptionSet works inside the Swift Compiler-->
<!--WRITEIT_POST_HTML_NAME=how-optionset-works-inside-the-swift-compiler-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-04-12T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2019-12-03T06:00:00+00:00-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITEIT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=Swift's OptionSet protocol is the bridged version of Objective-C's NS_OPTIONS enum -- a handy tool that allows you to very efficiently define a combination of "options" that can be used for a specific operation. Let's see what this protocol is, why OptionSets are preferred over a regular Set in some cases and finally what compiler magics it possesses.-->
<title>How OptionSet works inside the Swift Compiler</title>
<div class="blog-post">
<div class="post-title-index">
<h1>How OptionSet works inside the Swift Compiler</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 03 Dec 2019
</div>
</div>
<p>Swift's <code>OptionSet</code> protocol is the bridged version of Objective-C's <code>NS_OPTIONS</code> enum -- a handy tool that allows you to very efficiently define a combination of "options" that can be used for a specific operation.</p>
<div class="sponsor-article-ad-auto hidden"></div>
<p>If for example we were developing a JSON serializer, it would be nice if we allowed the user to control how certain aspects of the serialization process happened, such as if the keys are sorted or if indentation should be applied to the final JSON. In fact, this is how <code>Foundation's</code> <code>JSONSerialization</code> works:</p>
<pre><code>try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])</code></pre>
<p>From a "list" of options (the quotes will make sense later!), the <code>OptionSet</code> protocol contains a <code>contains(_:)</code> method to see if a specific option is inside the "list". Having several options passed together means that all these options should be considered, and passing no options can be done through an empty array.</p>
<p>But despite the looks, the <code>options</code> parameter <b>is not an array</b> but a <code>WritingOptions</code> <b>struct</b> that conforms to the <code>OptionSet</code> protocol. How the hell does it behaves like a <code>Set</code> even though it is not?!</p>
<p>If you've been following this blog, you'll know that the answer is a bunch of Swift compiler magic. Let's see what this protocol is, why <code>OptionSets</code> are preferred over a regular <code>Set</code> in this case, and finally what magic is done to provide the final behavior.</p>
<h2>Context: Objective-C's <code>NS_OPTIONS</code></h2>
<p>Before analyzing the Swift compiler code that allows a regular <code>struct</code> to behave like a <code>Set</code>, let's see where <code>OptionSet</code> came from.</p>
<p>The Objective-C equivalent of <code>OptionSet</code> was the <code>NS_OPTIONS</code> macro, a glorified enum that allowed the same option-mixing gimmick. With it, you can define your options just like an integer enum, like these hypothetical options for browsing SwiftRocks articles:</p>
<pre><code>typedef NS_OPTIONS(NSUInteger, SwiftRocksOptions) {
DarkMode = 1 << 0,
NoJavaScript = 1 << 1,
NoImages = 1 << 2,
};</code></pre>
<p>However, instead of using sequential numbers like 0, 1 and 2 in a regular enum, the idea is that you would use <b>powers of two</b> (easier represented by the number one 1 left-shifted (<<) by some value.):</p>
<p>If the usage of powers of two sounds confusing, consider how a three-bit number <b>0</b> looks like in binary:</p>
<pre><code>0 0 0</code></pre>
<p>In contrast, this is how a three-bit number <b>7</b> looks like in binary:</p>
<pre><code>1 1 1</code></pre>
<p>Since we have four bits and each of these bits can be zeroes or ones, what if we treated each bit as an option?</p>
<p>The first bit (from the right) could be the first option (DarkMode), the second bit the second option (NoJavaScript), and the third bit the third option (NoImage). if a bit is set to <b>one</b>, it means that option is "selected". This means that we could represent the selection of DarkMode as the number <b>1</b> (0 0 1), the selection of NoJavaScript as the number <b>2</b> (0 1 0), the selection of NoImage as the number <b>4</b> (1 0 0), and a potential combination of NoJavascript and NoImages as the number <b>6</b> (1 1 0).</p>
<p>The process of creating such numbers is generally referred to as <b>bitmasking</b> and is a very efficient way to treat the existence/combination of arbitrary values. While using a regular Array or Set would require us to store the actual elements in memory, the bitmasked requires just the memory space of the integer itself used to process the mask.</p>
<p>Adding and checking options in a mask is done through bitwise operators. The bitwise OR operator (|) returns a number that has a specific bit set to one if any of the numbers in the operation do so, so since we're using powers of two, using it on two options return a number that represents their combination:</p>
<pre><code>NSUInteger options = SwiftRocksOptionsNoJavaScript | SwiftRocksOptionsNoImages
// 1 1 0 (6)</code></pre>
<p>On the other hand, the bitwise AND (&) operator returns a number that has a specific bit set to one only if <b>both</b> numbers in the operation do so. If we use it with the set of options along with the specific option that we're checking, because we're using powers of two, we'll get a value different from zero if the option exists in the set (or zero if it doesn't):</p>
<pre><code>(options & SwiftRocksOptionsDarkMode) != 0 // false
(options & SwiftRocksOptionsNoJavaScript) != 0 // true
(options & SwiftRocksOptionsNoImages) != 0 // true</code></pre>
<p>Option bitmasking is a very clever use of the binary representation of a number. While unrelated to the article, I like to mention that doing option bitmasks like this is also used as the (current) most efficient solution to the famous Traveling Salesman problem.</p>
<h2>How <code>NS_OPTIONS</code> is bridged to <code>OptionSet</code></h2>
<p>In Swift, you don't have to create enums and manually generate these masks. The <code>OptionSet</code> protocol abstracts the process of creating such bitmasks. Hooray!</p>
<p>But what about things that come from Objective-C? If the Clang importer section of the compiler detects a <code>NS_OPTIONS</code> macro, it will convert it to a <code>struct</code> that inherits from <code>OptionSet</code>. <code>OptionSet</code> itself inherits from <code>RawRepresentable</code>, so the enum elements are converted into <code>static let</code> properties that take a <code>rawValue</code>. Here's an example of a converted <code>JSONSerialization.WritingOptions</code>:</p>
<pre><code>public struct WritingOptions: OptionSet {
public typealias RawValue = Int
public let rawValue: RawValue
public init(rawValue: RawValue) {
self.rawValue = rawValue
}
public static let prettyPrinted = WritingOptions(rawValue: 1 << 0)
public static let sortedKeys = WritingOptions(rawValue: 1 << 1)
public static let fragmentsAllowed = WritingOptions(rawValue: 1 << 2)
public static let withoutEscapingSlashes = WritingOptions(rawValue: 1 << 3)
}</code></pre>
<h2>How OptionSet is defined in the Standard Library</h2>
<p>Although the <code>OptionSet</code> is used with integers and bitmasking, it can, in theory, be used with anything else as the protocol is just a <code>rawValue</code> and some algebra methods:</p>
<pre><code>public protocol OptionSet: RawRepresentable, SetAlgebra {
associatedtype Element = Self // from SetAlgebra
init(rawValue: Self.RawValue)
public func union(_ other: Self) -> Self
public func intersection(_ other: Self) -> Self
public func symmetricDifference(_ other: Self) -> Self
public func contains(_ member: Self) -> Bool
public mutating func insert(_ newMember: Self.Element) -> (Bool, Self.Element)
public mutating func remove(_ member: Self.Element) -> Self.Element?
public mutating func update(with newMember: Self.Element) -> Self.Element?</code></pre>
<p>However, implementing these methods isn't required -- if the <code>rawValue</code> type is a number that isn't a floating-point, you'll get default implementations in the form of the bitmasking techniques seen before!</p>
<pre><code>public mutating func formUnion(_ other: Self) {
self = Self(rawValue: self.rawValue | other.rawValue)
}
public mutating func formIntersection(_ other: Self) {
self = Self(rawValue: self.rawValue & other.rawValue)
}</code></pre>
<h2>OptionSet Syntax Sugars</h2>
<p>We've seen how to work with <code>OptionSet</code> through its methods, but where does this behavior come from?</p>
<pre><code>options: [.prettyPrinted, .sortedKeys]</code></pre>
<p>Even though the options argument is the struct itself, we're sending an array of them to the type -- and it works.</p>
<p>Interestingly enough this syntax sugar has nothing to do with <code>OptionSet</code> itself. IT comes from an inner protocol of which it inherits from: <code>SetAlgebra</code>.</p>
<p>The <code>SetAlgebra</code> protocol is the backbone for mathematical set operations in the Standard Library and is used by types like <code>Set</code> and <code>OptionSet</code>:</p>
<pre><code>public protocol SetAlgebra: Equatable {
associatedtype Element
public func union(_ other: Self) -> Self
public func intersection(_ other: Self) -> Self
public func symmetricDifference(_ other: Self) -> Self
public func contains(_ member: Self) -> Bool
...</code></pre>
<p>(This looks very similar to <code>OptionSet</code> itself, and is because <code>OptionSet</code> can be seen as just a light abstraction of <code>SetAlgebra</code> that provides the bitmasking features through a rawValue.)</p>
<p>The magic comes from this little extension:</p>
<pre><code>extension SetAlgebra: ExpressibleByArrayLiteral {
public init(arrayLiteral: Element...) {
self.init(arrayLiteral)
}
public init<S: Sequence>(_ sequence: S) where S.Element == Element {
self.init()
for e in sequence { insert(e) }
}
}</code></pre>
<p><code>SetAlgebra</code> inherits from <code>ExpressibleByArrayLiteral</code>, which allow it to be represented directly as an array. This triggers an <code>insert()</code> call for every element, which for our <code>OptionSet</code> types means that we'll progressively trigger bitwise AND (|) operations, resulting in a final <code>OptionSet</code> that represents the combination of all the options. Hooray!</p>
<h2>How <code>ExpressibleByArrayLiteral</code> works internally</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p><a href="https://swiftrocks.com/swift-expressibleby-protocols-how-they-work-internally-in-the-compiler">The innards of the ExpressibleBy family are covered in detail in my previous article,</a> but as a short explanation, we can say that when the type checker finds a literal being attributed to an explicit type, it checks if it conforms to the related <code>ExpressibleBy</code> protocol and coverts the expression to the designated initializer from the protocol if it does (and throws a compilation error if it does not). For more details, check the linked article.</p>
<h2>Conclusion</h2>
<p>The <code>OptionSet</code> protocol itself is hardly used nowadays, but its existence has some history and shows some of the importance of analyzing what you're developing in terms of performance. You can always use a regular <b>Array</b> or <b>Set</b> to define these options, but should you? For these case where you have a limited set of unique options, using a bitmask is considerably quicker and less resource-intensive, something which your user's device will be very grateful for :)</p>
<h2>References and Good reads</h2>
<a href="https://github.com/apple/swift">The Swift Source Code</a>
<br>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism4.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>