EP 54 · Standalone · Apr 15, 2019 ·Members

Video #54: Advanced Swift Syntax Enum Properties

smart_display

Loading stream…

Video #54: Advanced Swift Syntax Enum Properties

Episode: Video #54 Date: Apr 15, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep54-advanced-swift-syntax-enum-properties

Episode thumbnail

Description

This week we’ll put the finishing touches on our enum property code generation tool. We’ll add support for enum cases with multiple associated values and enum cases with no associated values, and we’ll add a feature that will make enums even more ergonomic to work with!

Video

Cloudflare Stream video ID: b8d2b335c585b01b7eb4788db259e9a5 Local file: video_54_advanced-swift-syntax-enum-properties.mp4 *(download with --video 54)*

Transcript

0:05

Last time we set out to automate all the boilerplate involved in creating what we call “enum properties”, which are computed properties on enums that give us easy access to the data held inside the enum’s cases. We’re doing this because Swift makes it very easy to access data in structs, but does not give us many tools for doing the same with enums.

0:30

To achieve this we started creating a package with the Swift Package Manager. It has a dependency on SwiftSyntax, which is Apple’s library for parsing Swift source code into a tree of tokens that we can traverse and inspect. We ended up with a playground that is capable of parsing the code in a Swift file, finding all the enums in it, and creating computed properties for each of the cases in those enums.

1:01

That’s awesome, but there are still a lot of subtle edge cases that we are not accounting for, and so we can’t really point this tool at a real world code base and expect it to work.

1:19

So today we are going handle more of those edge cases so that we will be in a good position to extract everything out of the playground and make a real command line tool out of it!

1:51

Let’s take a look at what we’ve written so far. import SwiftSyntax import Foundation let url = Bundle.main.url( forResource: "Enums", withExtension: "swift" )! let tree = try SyntaxTreeParser.parse(url) class Visitor: SyntaxVisitor { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print("extension \(node.identifier) {") return .visitChildren } override func visitPost(_ node: Syntax) { if node is EnumDeclSyntax { print("}") } } override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(" var \(node.identifier): (\(node.associatedValue!.parameterList))? {") print(" guard case let .\(node.identifier)(value) = self else { return nil }") print(" return value") print(" }") } } let visitor = Visitor() tree.walk(visitor) // extension Validated { // var valid: Valid? { // guard case let .valid(value) = self else { return nil } // return value // } // var invalid: [Invalid]? { // guard case let .invalid(value) = self else { return nil } // return value // } // }

2:58

Now this tool is very useful but currently only works for enums where every case has a single associated value. In the case of multiple associated values or no associated values, we would be generating invalid code. So today we’re going to address a bunch of these shortcomings to end up with a tool that we can run on code bases with many different kinds of enums in various shapes that should always generate correct, compiling code. Supporting multiple associated values

3:09

Let’s start by addressing enums with cases that contain multiple associated values. We’ll need to update the source file we’ve been generating code against with an example. // enum Validated<Valid, Invalid> { // case valid(Valid) // case invalid([Invalid]) // } enum Node { case element(String, [String: String], [Node]) case text(String) }

3:29

I’ve commented out our first example so that we can focus just on this new case. It’s called Node and represents an HTML or XML node. We used a similar representation in an episode where we built a domain specific language for HTML using Swift. It has two cases:

3:47

An element case with three associated values representing an element’s tag, attributes, and child nodes.

3:59

A text case representing a text node with String content.

4:07

When we run our playground, we get new output: extension Node { var element: String, [String: String], [Node]? { guard case let .element(value) = self else { return nil } return value } var text: String? { guard case let .text(value) = self else { return nil } return value } }

4:14

Now this isn’t quite right, but it’s pretty close! The element property is returning a list of associated types, which isn’t valid syntax. What’s a bit stranger is some extra whitespace after the type name. extension Node {

4:31

Let’s address that first by exploring the enum’s identifier, which is of type TokenSyntax . It contains a bunch of interesting methods and properties around “trivia”, which represents any whitespace around a token, like spaces and newlines. In the case of our definition of Node , we have a trailing space before we open things with curly braces, and that information is retained in this token.

5:23

SwiftSyntax handily provides a means of stripping trivia from identifiers. override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print("extension \(node.identifier.withoutTrivia()) {") return .visitChildren }

5:31

And now the extension is printing a bit more nicely: extension Node {

5:37

It’s nice to see that SwiftSyntax provides a bunch of convenience around reformatting and prettifying code. Swift may even come with a formatter tool in the future, and it will use SwiftSyntax to rewrite trivia in nice, consistent ways.

5:50

But we’re still generating invalid code: var element: String, [String: String], [Node]? { guard case let .element(value) = self else { return nil } return value }

5:56

In order to return each associated value of element we need to package them all up into a tuple. Lucky for us all that requires is some parens, and those parens come for free if we return associatedValue directly! override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(" var \(node.identifier): \(node.associatedValue!.elementList)? {") print(" guard case let .\(node.identifier)(value) = self else { return nil }") print(" return value") print(" }") return .skipChildren } Now we could have taken the parameter list, which is a Sequence , map over each value, pluck out the type, and then join them all with commas, but this shortcut seems good enough for now. We encourage the viewer to load up a playground and play around, though!

6:23

Our output is looking a lot better: extension Node { var element: (String, [String: String], [Node])? { guard case let .element(value) = self else { return nil } return value } var text: (String)? { guard case let .text(value) = self else { return nil } return value } }

6:32

The fact that String is wrapped in parens looks a lil off, but it’ll compile just fine, so let’s copy-paste our Node type into the playground and copy-paste the output of our generated code to take it for a spin.

6:53

Let’s make an HTML node value we can play with. let link = Node.element( "a", ["href": "/"], [.text("There's no place like home!")] )

7:05

We can effortlessly traverse into it to pluck out specific data. For instance, the tag, which is at the tuple’s 0 th index: link.element?.0 // "a"

7:18

We can just as easily grab the attributes: link.element?.1 // ["href": "/"]

7:26

And the child nodes: link.element?.2 // [{text "There's no place like home"}]

7:33

We can also traverse more deeply into the child nodes to extract text using the text property. link.element?.2.compactMap { $0.text } // ["There's no place like home!"]

8:00

Traversing into the tuple result by integer offset isn’t the most pleasant experience. It’s not super apparent what the 0th or 2nd value is by reading this code alone. We can address this by updating our Node definition with some parameter names. enum Node { case element( tag: String, attributes: [String: String], children: [Node] ) case text(String) }

8:26

And update our value accordingly. let link = Node.element( tag: "a", attributes: ["href": "/"], children: [.text("There's no place like home!")] )

8:42

And when we re-run our code gen, it all just works! extension Node { var element: ( tag: String, attributes: [String: String], children: [Node] )? { guard case let .element(value) = self else { return nil } return value }

8:50

Now we can re-paste in our updated Node code and the traversal becomes a lot more readable. link.element?.tag // "a" link.element?.attributes // ["href": "/"] link.element?.children.compactMap { $0.text } // ["There's no place like home!"]

9:09

These labels introduce an edge case that we need to address. Let’s say we added a label to Node ’s text case: enum Node { case element( tag: String, attributes: [String: String], children: [Node] ) case text(content: String) }

9:24

This is a perfectly valid thing to do in Swift, and even helps the compiler name let bindings for you if you have it generate the missing cases of a switch statement.

9:32

We can make a small change to get things compiling again. let link = Node.element( tag: "a", attributes: ["href": "/"], children: [.text(content: "There's no place like home!")] )

9:41

But the output of our code generation: extension Node { var element: ( tag: String, attributes: [String: String], children: [Node] )? { guard case let .element(value) = self else { return nil } return value } var text: (content: String)? { guard case let .text(value) = self else { return nil } return value } } Cannot create a single-element tuple with an element label It’s invalid. Swift does not let you create single-element tuples, which means we can’t write code that returns a tuple with a single label. When we were returning a String wrapped in parentheses it may have looked like a single-element tuple, but the parens were purely decorative.

10:20

The fix isn’t so bad. We can check the length of the parameter list, and if it only has a single element, we can pluck out that element and force-unwrap the type, otherwise we return the associatedValue whole, as before. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType = node.associatedValue!.parameterList.count == 1 ? "\(node.associatedValue!.parameterList[0].type!)" : "\(node.associatedValue!.parameterList)" print(" var \(node.identifier): (\(propertyType))? {")

11:50

Now we have a tool that can generate completely valid code for the Node type, and we’ve even cleaned up those parens from earlier. extension Node { var element: ( tag: String, attributes: [String: String], children: [Node] )? { guard case let .element(value) = self else { return nil } return value } var text: (String)? { guard case let .text(value) = self else { return nil } return value } }

11:58

And now that we’re handling each case separately, we can get rid of those decorative parens. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType = node.associatedValue!.parameterList.count == 1 ? "\(node.associatedValue!.parameterList[0].type!)" : "(\(node.associatedValue!.parameterList))" print(" var \(node.identifier): \(propertyType)? {")

12:13

And now the code that is generated looks a bit more real-world: extension Node { var element: ( tag: String, attributes: [String: String], children: [Node] )? { guard case let .element(value) = self else { return nil } return value } var text: String? { guard case let .text(value) = self else { return nil } return value } } Supporting no associated values

12:24

With a small change we were able to support enums with multiple associated values.

13:00

So what about enums with no associated values?

13:21

Unfortunately, our tool does not handle this case well. Let’s start by defining an enum that has some cases with no associated values. // enum Validated<Valid, Invalid> { // case valid(Valid) // case invalid([Invalid]) // } // // enum Node { // case element( // tag: String, attributes: [String: String], children: [Node] // ) // case text(content: String) // } enum Fetched<A> { case cancelled case data(A) case failed case loading } It represents a loadable value that can be in any one of several states.

14:31

When we run our playground: extension Fetched { Fatal error: Unexpectedly found nil while unwrapping an Optional value We get one line of output before we get a runtime crash.

14:33

We’re currently force-unwrapping the enum case’s associated value, but we’re working with a loading case that doesn’t have one, so it’s nil . Rather than force-unwrap associatedValue , let’s safely unwrap it and separately handle the nil case.

14:56

First, we know we need a string representation of the property’s return type, and then in each case of our switch we assign it. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType: String if let associatedValue = node.associatedValue { propertyType = associatedValue.parameterList.count == 1 ? "\(associatedValue.parameterList[0].type!)" : "\(associatedValue)" } else { propertyType = <#???#> }

15:30

What value can we use to denote a property without an associated type? We can use the Void as a kind of placeholder to mean: if I am a value of this case, return Void , otherwise return nil . override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType: String if let associatedValue = node.associatedValue { propertyType = associatedValue.parameterList.count == 1 ? "\(associatedValue.parameterList[0].type!)" : "\(associatedValue)" } else { propertyType = "Void" }

15:59

When we run our playground, our output is looking better. extension Fetched { var cancelled: Void? { guard case let .cancelled(value) = self else { return nil } return value } var data: A? { guard case let .data(value) = self else { return nil } return value } var failed: Void? { guard case let .failed(value) = self else { return nil } return value } var loading: Void? { guard case let .loading(value) = self else { return nil } return value } } But it’s still not quite right. Though enum cases without associated values are like Void , Swift doesn’t allow you to pattern match and extract that value.

16:29

If we paste it into our playground, we can verify. Pattern with associated values does not match enum case ‘cancelled’

17:04

We are trying to unwrap an associated value that doesn’t exist.

17:09

Instead of let -binding, we just want to case match. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType: String let pattern: String if let associatedValue = node.associatedValue { propertyType = associatedValue.parameterList.count == 1 ? "\(associatedValue.parameterList[0].type!)" : "\(associatedValue)" pattern = "guard case let .\(node.identifier)(value)" } else { propertyType = "Void" pattern = "guard case .\(node.identifier)" } print(" var \(node.identifier): \(propertyType)? {") print(" \(pattern) = self else { return nil }") print(" return value")

18:40

Looking better, but still not quite right: extension Fetched { var cancelled: Void? { guard case .cancelled = self else { return nil } return value } var data: A? { guard case let .data(value) = self else { return nil } return value } var failed: Void? { guard case .failed = self else { return nil } return value } var loading: Void? { guard case .loading = self else { return nil } return value } }

19:00

We no longer have a value to return, so we should just return an instance of Void instead, which is denoted using () . override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { let propertyType: String let pattern: String let returnString: String if let associatedValue = node.associatedValue { propertyType = associatedValue.parameterList.count == 1 ? "\(associatedValue.parameterList[0].type!)" : "\(associatedValue)" pattern = "guard case let .\(node.identifier)(value)" returnString = "value" } else { propertyType = "Void" pattern = "guard case .\(node.identifier)" returnString = "()" } print(" var \(node.identifier): \(propertyType)? {") print(" \(pattern) = self else { return nil }")

19:46

And finally, when we run our playground again: extension Fetched { var cancelled: Void? { guard case .cancelled = self else { return nil } return () } var data: A? { guard case let .data(value) = self else { return nil } return value } var failed: Void? { guard case .failed = self else { return nil } return () } var loading: Void? { guard case .loading = self else { return nil } return () } } We get output that is looking pretty good!

20:08

Let’s paste it into our playground and take it for a spin! let data = Fetched<Int>.loading data.loading // () data.cancelled // nil

20:43

And if we give it data, we can access the associated value let data = Fetched<Int>.data(42) data.loading // nil data.cancelled // nil data.data // 42

21:00

What if we were working with an array of requests? let requests: [Fetched<String>] = [ .cancelled, .data("Blob's Travel Blog"), .failed, .loading, .data("Blob's Food Blog"), .loading, ]

21:16

Now we can take this array and extract all the data: requests .compactMap { $0.data } // ["Blob's Travel Blog", "Blob's Food Blog"]

21:36

What if we want to know how many requests failed? requests .filter { $0.failed != nil } .count // 1

21:58

It’s worth noting that we couldn’t have just used == : data == .failed Binary operator ‘==’ cannot be applied to operands of type ‘Loading<String>’ and ‘_’

22:20

And this is because Loading does not conform to Equatable . Now in this example we could conditionally extend Loading to be Equatable where its associated value is Equatable , but we still wouldn’t be able to use == wherever the value being loaded is not Equatable . Meanwhile, our Void property gives us a universal solution! Adding boolean properties

23:09

Things are looking really good now! We’re generating a ton of properties for a ton of different kinds of enum cases.

23:20

One thing that might be nice, though, it to tidy up the interface for testing a value is a certain case.

24:02

Right now it’s a nil check: requests .filter { $0.failed != nil } .count

24:09

But what might be nice is generate a Boolean property helper alongside each enum property, so that we could write the following instead: requests // .filter { $0.isFailed } .filter { $0.failed != nil } .count

24:16

It turns out this isn’t so hard to do! print(" var is\(node.identifier): Bool {") print(" return self.\(node.identifier) != nil") print(" }")

25:07

When we run it, we get some new boolean properties: extension Fetched { var cancelled: Void? { guard case .cancelled = self else { return nil } return () } var iscancelled: Void? { return self.cancelled != nil } var data: A? { guard case let .data(value) = self else { return nil } return value } var isdata: Void? { return self.cancelled != nil } var failed: Void? { guard case .failed = self else { return nil } return () } var isfailed: Void? { return self.cancelled != nil } var loading: Void? { guard case .loading = self else { return nil } return () } var isloading: Void? { return self.cancelled != nil } } But it doesn’t read super nice. It’d be good to capitalize the identifier so that we get the camel-casing we expect in Swift.

25:22

To do so doesn’t require a ton of work. While we may be tempted to use capitalized and call it a day. let capitalizedIdentifier = "\(node.identifier)".capitalized This isn’t quite what we want because even though it will capitalize the first letter, it will also lowercase everything else. We want to preserve any camel-casing of existing identifiers.

25:51

Instead, we can use a mixture of first and dropFirst() . let identifier = "\(node.identifier)" let capitalizedIdentifier = "\(identifier.first!.uppercased())\(identifier.dropFirst())" print(" var is\(capitalizedIdentifier): Bool {") print(" return self.\(node.identifier) != nil") print(" }") The majority of this function was copy-pasted from our earlier one. We just had to tweak a few things and write some code to capitalize the identifier so it prints nicely.

26:42

Now when we hop over to the console everything’s printing much more nicely. extension Fetched { var cancelled: Void? { guard case .cancelled = self else { return nil } return () } var isCancelled: Void? { return self.cancelled != nil } var data: A? { guard case let .data(value) = self else { return nil } return value } var isData: Void? { return self.cancelled != nil } var failed: Void? { guard case .failed = self else { return nil } return () } var isFailed: Void? { return self.cancelled != nil } var loading: Void? { guard case .loading = self else { return nil } return () } var isLoading: Void? { return self.cancelled != nil } }

26:59

And at the call site it can read a bit nicer. requests .filter { $0.isFailed } .count What’s the point?

27:28

Alright, this is really getting somewhere, but maybe we should take a step back and evaluate things. We now have a tool that can generate enum properties from Swift source code, but we’ve gotten a bit in the weeds accounting for some edge cases, and we even mentioned that there are more edge cases out there. So does it make sense to continue down this path?

28:08

It’s true, we have gone a bit into the weeds, but we’ve seen how nice these enum properties can be, and it’s a totally worthwhile pursuit to try to solve the problem.

28:28

To really appreciate what we’ve accomplished, let’s uncomment all of our enums and take a look at the combined output of our generator: extension Validated { var valid: Valid? { guard case let .valid(value) = self else { return nil } return value } var isValid: Bool { return self.valid != nil } var invalid: [Invalid]? { guard case let .invalid(value) = self else { return nil } return value } var isInvalid: Bool { return self.invalid != nil } } extension Node { var element: ( tag: String, attributes: [String: String], children: [Node] )? { guard case let .element(value) = self else { return nil } return value } var isElement: Bool { return self.element != nil } var text: String? { guard case let .text(value) = self else { return nil } return value } var isText: Bool { return self.text != nil } } extension Fetched { var cancelled: Void? { guard case .cancelled = self else { return nil } return () } var isCancelled: Void? { return self.cancelled != nil } var data: A? { guard case let .data(value) = self else { return nil } return value } var isData: Void? { return self.cancelled != nil } var failed: Void? { guard case .failed = self else { return nil } return () } var isFailed: Void? { return self.cancelled != nil } var loading: Void? { guard case .loading = self else { return nil } return () } var isLoading: Void? { return self.cancelled != nil } } That’s a lot! Over fifty lines of code that we didn’t have to write ourselves. And this is for three relatively short enums. Imagine the output for a large code base! This is a tool that will pay tons of dividends over time. And without it we’d be stuck hand-writing this logic, sprinkled throughout our code bases. While we think that the Swift compiler should solve this problem, we can be empowered in the meantime to use code generation as a means of solving the problem ourselves in the meantime.

29:42

So that’s “what’s the point”: we started by showing that enums should have an easy way of accessing their data because struct data access is incredibly easy, and enums and structs are complementary ideas. Then we looked to SwiftSyntax, which just recently became a usable tool, to solve the problem, so that with a little bit of up-front work we can make enum data access super accessible throughout our code bases and on-par with their struct counterparts.

30:44

Now we’re not quite done yet. While our code works, it’s still stuck in a playground where we can’t even use it! It’s also completely untestable: we’re printing directly to the console, after all. Next time we’ll extract this code to a library, write some snapshot tests for it, and build a command line tool that we can use in our own projects. Till then! Downloads Sample code 0054-advanced-swift-syntax-enum-properties Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .