EP 52 · Standalone · Apr 1, 2019 ·Members

Video #52: Enum Properties

smart_display

Loading stream…

Video #52: Enum Properties

Episode: Video #52 Date: Apr 1, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep52-enum-properties

Episode thumbnail

Description

Swift makes it easy for us to access the data inside a struct via dot-syntax and key-paths, but enums are provided no such affordances. This week we correct that deficiency by defining the concept of “enum properties”, which will give us an expressive way to dive deep into the data inside our enums.

Video

Cloudflare Stream video ID: 02caf02bc4175cd7c6a1aef2d883be53 Local file: video_52_enum-properties.mp4 *(download with --video 52)*

References

Transcript

0:05

Last time we revisited a topic that we know and love: algebraic data types. We explored how in the Swift type system Swift, multiplication manifests itself in product types, like structs and tuples, and addition manifests itself in sum types, like enums. We saw that, just like in algebra, addition and multiplication, and structs and enums are just two sides of the same coin: no one is more important than the other. And we saw that many of the things we love about structs do in fact have a corresponding feature on enums, and vice versa.

0:37

However, we also saw that product types definitely see some favoritism over enums in the language design. We saw this especially in how Swift gives us anonymous structs in the form of tuples, but there is no corresponding anonymous enum type. We theorized how it might look and we may even have it in the language some day because it’d be a really nice tool to have.

1:05

Now anonymous sum types isn’t something we can fix ourselves, but there are other examples of struct favoritism that we can correct by bringing enums up to the same level. Accessing product and sum data

1:24

The application we will be looking at is how we access the data inside our structs and enums. Turns out Swift slightly favors structs over enums, because it is very easy to access data inside a struct, but pretty annoying to do the same for enums. We are going to look at this problem in depth, and then see how we can solve it.

1:44

Struct data is stored in properties. struct User { var name: String var isAdmin: Bool }

1:46

Here we have a User struct that has a name property holding a String and an isAdmin property holding a Bool .

1:56

We can create a User by calling its initializer and passing it values to store in its properties. let user = User(name: "Blob", isAdmin: true)

2:07

And we can easily access that data using dot syntax. user.name // "Blob" user.isAdmin // true

2:21

Enum data, meanwhile, is stored in associated values. enum Validated<Valid, Invalid> { case valid(Valid) case invalid([Invalid]) }

2:25

Here’s the Validated enum that we’ve covered in past episodes. It’s a Result -like type that can handle multiple things that may fail, making it the perfect thing for validating form data. It has two separate cases, valid , which can hold an associated value representing something that was successfully validated, and invalid , which can hold an associated array of validation errors.

3:00

If we have a Validated value at hand, say a valid value holding the number 42 : let validValue = Validated<Int, String>.valid(42)

3:29

We can access its data using pattern matching. if case let .valid(value) = validUser { value // 42 }

3:57

Swift’s pattern matching is a seriously powerful tool that we love, but its syntax can be quite difficult to master. In fact, there are entire websites devoted to demystifying the complexities!

4:13

Dot syntax, on the other hand, could hardly be simpler. Looking at things side by side the difference is clear: user.name // "Blob" if case let .valid(value) = validValue { value // 42 }

4:27

Beyond having to remember the syntax and quirks of if case let , there’s just a lot more noise and ceremony. Three lines instead of one, where we must assign the data to a variable, open a scope where we have access to that data, and finally close that scope.

4:47

If we wanted to have access to that data outside of the scope, we need to do even more. user.name // "Blob" let optionalValue: Int? if case let .valid(value) = validValue { optionalValue = value } else { optionalValue = nil } optionalValue // Optional(42)

5:21

Yikes! Three lines was bad enough, but now we’re up to seven! And six of those lines are statements, most of which are just assigning data to temporary variables. So not only do we have to remember the syntax and quirks of if case let , we end up with a lot more code. Meanwhile, user.name is a short, single-line expression that didn’t require us to bind the data to a new variable.

5:57

Because accessing enum data requires statements, it leads to super imperative code. In functional programming, we love expressions because they lend themselves nicely to composition.

6:07

For example, let’s build an array of User s and do some work on them. let users = [ User(name: "Blob", isAdmin: true), User(name: "Blob, Sr.", isAdmin: true), User(name: "Blob, Jr.", isAdmin: false) ]

6:11

We can call filter on them to filter out the non-admins. users .filter { $0.isAdmin } // [ // User(name: "Blob", isAdmin: true), // User(name: "Blob, Sr.", isAdmin: true) // ] Because User is a struct it was super easy to run this filter via that dot syntax.

6:22

From there we can chain along with a map to get the names of our admins. users .filter { $0.isAdmin } .map { $0.name } // ["Blob", "Blob, Sr."]

6:32

This is super simple and succinct with struct property access. Arrays of enums

6:52

So what does it look like to work with an array of enums? let validatedValues: [Validated<Int, String>] = [ .valid(1), .invalid(["Failed to calculate value"]), .valid(42) ]

7:00

Here we have a bunch of validated values: some valid, some invalid.

7:16

What if we wanted to filter out all the invalid values? validatedValues .filter { guard case .valid = $0 else { return false } return true } // [valid(1), valid(42)] Here we’re using case without let in order to check if the validated value is a valid one. We’re still pattern matching using a statement, though, and it’s all verbose and pretty gross, especially when compared with filtering users: users .filter { $0.isAdmin }

8:33

And really, this filter ing isn’t even super helpful: everything’s still wrapped up in a Validated value, so if we chain along again with another transformation, we’re going to have to do that case let dance another time.

8:42

So instead of filter ing, we could use map with a transform function that unwraps valid values. validatedValues .map { guard case let .valid(value) = $0 else { return nil } return value } Unable to infer complex closure return type; add explicit type to disambiguate

9:06

Yeesh. It turns out that Swift can’t infer what the return type of a multiline closure is. With filter this wasn’t a problem because the return type is locked to Bool , but map can transform a value into anything, so we need to make the return type explicit.

9:29

One way to make the return type explicit is to add an internal annotation. In doing so we need to give our shorthand $0 syntax an explicit name. validatedValues .map { validated -> Int? in guard case let .valid(value) = validated else { return nil } return value } // [1, nil, 42]

9:48

And so we’re stuck with even more work than where we map ped an array of structs: we have to name our enum value, specify an explicit return type, name the enum’s associated value in a statement, and deal with a lot of explicit return statements.

10:05

This compiles, but it’s still not quite right. Each of these values is an optional Int , but it makes more sense to work with unwrapped Int s, so what we actually want to use is compactMap , which transforms an array of elements into optional values, removes all the nil s, and finally returns an array of non-optional values. validatedValues .compactMap { validated -> Int? in guard case let .valid(value) = validated else { return nil } return value } // [1, 42]

10:28

So here we have it: the shortest, most concise way to take an array of enums and pluck out the associated values of a specific case.

10:39

Let’s compare with the struct approach over a single operation: users .map { $0.name }

10:57

This map over structs is a one-liner that takes advantage of two really nice features: $0 shorthand syntax and implicit return s. The entirety of the closure describes the value being transformed and nothing more.

11:16

Meanwhile, our compactMap over enums requires pattern matching, which requires us to spill over multiple lines, which requires us add an explicit return on all the branches, and requires us to add an explicit return type, which requires us to abandon $0 shorthand syntax and name our temporary variable. The one part we care about is the case name, valid , which describes the value we want to pluck out of the enum. The rest is just noise! Structs and key paths

11:45

And we should note that struct property access can be even more succinct. Every property comes with a key path: compiler-generated code that we can use to access property values.

12:05

In a past episode on “ functional getters ” we defined a helper that promotes any key path to a getter function. prefix operator ^ prefix func ^ <Root, Value>( _ keyPath: KeyPath<Root, Value> ) -> (Root) -> Value { return { root in root[keyPath: keyPath] } }

12:19

This allows us to write our filter and map in an even more succinct fashion. users .filter(^\.isAdmin) .map(^\.name)

12:37

This clears away even more noise: no need to open a closure with curly braces or refer to a value using $0 .

12:52

In Swift 5.1, we will get this kind of key path promotion for free, saving us another character. users .filter(\.isAdmin) .map(\.name)

13:07

Swift doesn’t provide any kind of key path analog for enum cases, so yet again we see where the language is giving structs more ergonomics and sugar than enums. Accessing Result data

13:29

Let’s briefly take a look at another enum that will come with the Swift 5 standard library: the Result type! It looks very much like the Validated type we defined. enum Result<Success, Failure: Error> { case success(Success) case failure(Failure) } And its API was even designed to provide a convenient means of accessing the value.

14:04

Let’s consider the following array of results: let results: [Result<Int, Error>] = [ .success(1), .failure(NSError(domain: "co.pointfree", code: -1, userInfo: nil)), .success(42) ]

14:15

But a nice thing about Result is that it comes with a get method that unwraps a successful value or throws a failure.

14:35

We can combine this method with try? to safely filter out any failures using compactMap . results .compactMap { try? $0.get() } // [1, 42]

15:08

If we compare it to the work we did to get access to a validated value, it’s no contest. validatedValues .compactMap { validated -> Int? in guard case let .valid(value) = validated else { return nil } return value } // [1, 42]

15:15

Swift 5’s Result type is clearly giving us something far more succinct and far more first-class for the Result type than all the other enum types out there. It may feel like enums are finally even catching up!

15:35

But it’s important to point out that this API was defined in a completely ad hoc fashion by the standard library. And no complementary API exists for plucking out a failure! This leaves the Result type feeling imbalanced, where accessing success is privileged over accessing failure. If we wanted to transform an array of results into an array of failures, we’re back to cumbersomely pattern matching: results .compactMap { result -> Error? in guard case let .failure(error) = result else { return nil } return error }

16:35

So even though Swift has provided an affordance to make it easier to pluck out a value from a particular enum, every other enum still suffers. Recovering enum ergonomics

17:09

Luckily there is a more general solution to this ergonomic imbalance, and we can get there by analyzing what makes struct access simpler and asking if we can transport it back to enums.

17:45

Structs use property access, so a question we could ask is “what would enum properties look like?” What would it be like to access associated data from an enum by using properties?

17:53

Enums don’t have property storage, but we can define computed properties on them, so let’s try to define a property on Validated that unwraps and returns the associated value of a valid case. enum Validated<Valid, Invalid> { case valid(Valid) case invalid([Invalid]) var valid: Valid { } }

18:17

A Validated value can only return Valid data from valid cases, so the return type needs to be optional. var valid: Valid? { }

18:31

And the body of this is similar to all of our pattern matches from before. We just need to pattern match on self . var valid: Valid? { guard case let .valid(value) = self else { return nil } return value }

18:47

With this property defined we can rewrite our compactMap to be much simpler. validatedValues .compactMap { $0.valid } // [1, 42]

19:10

We’ve recovered all of the things we lost earlier: we’re back to a single, succinct expression that takes advantage of $0 shorthand syntax and implicit return s and return types.

19:19

And in defining this computed property we get something else: a key path! \Validated<Int, String>.valid // KeyPath<Validated<Int, String>, Int?>

19:42

While it’s easy to associate key paths with struct s, they more generally apply to any kind of property access, which means we can yet again make things even more succinct: validatedValues .compactMap(^\.valid) // [1, 42]

20:06

And in defining this property for the valid case just once we get all the ergonomics of struct property access. It was super simple to do, too, just a bit of boilerplate.

20:20

Defining a property for invalid cases is a matter of copy-paste where we change the case name and return type. var invalid: [Invalid]? { guard case let .invalid(value) = self else { return nil } return value }

20:42

And now we can just as easily gather up all of our invalid errors. validatedValues .compactMap { $0.invalid } // [["Failed to calculate value"]]

20:55

Or, with key paths: validatedValues .compactMap(^\.invalid) // [["Failed to calculate value"]]

21:04

A nice thing about key paths is that they compose nicely and we can optionally chain to grab a deeper value. For example, given an array of validated users: let validatedUsers: [Validated<User, String>] = [ .valid(User(name: "Blob", isAdmin: true)), .invalid(["Blob Jr. is not old enough"]), .valid(User(name: "Blob, Sr.", isAdmin: true)), ]

21:45

With enum properties, we can traverse over these validated users and compact map them to grab each of names. validatedUsers .compactMap(^\.valid?.name) // ["Blob", "Blob, Sr."] What’s the point?

22:06

So we’re starting to see how cool this idea of enum properties is: it allows us to make working with enum data just as ergonomic as struct data, key paths and all! All we had to do was define the properties once up front and this is where the boilerplate lives.

22:27

This is interesting and all, but let’s ask “what’s the point?”: because there’s the potential for a lot of boilerplate here. We defined two properties in this episode, but we have plenty more enums and enum cases in our code bases and does it really make sense to define perhaps hundreds of these properties? That’s a hard ask.

22:58

True, this is a lot of boilerplate, and it’s easy to get wrong or forget to write, and it’s a bummer that the compiler gives all of these affordances to structs for free, but we have to take matters into our own hands when it comes to enums.

23:19

However, just in seeing how much nicer enums can get, we are empowered to solve the problem, and we could probably solve it in a nicer way than manually typing out each enum property. So let’s turn to code generation.

23:32

Code generation is a really powerful tool: it allows you to close the gap between what the compiler gives you and what you would have to otherwise manage tediously by hand. Even the Swift standard library has a code generation tool called “

GYB 23:54

Enum properties are 100% boilerplate. Ideally the compiler would give them to us for free, but in the meantime we can use a code generation tool to do the work for us. Next time we’re going to use another tool that Apple ships, called SwiftSyntax , to do that work. Till next time! References Getters and Key Paths Brandon Williams & Stephen Celis • Mar 19, 2018 An episode dedicated to property access on structs and how key paths can further aid us in writing expressive code. Note Key paths aren’t just for setting. They also assist in getting values inside nested structures in a composable way. This can be powerful, allowing us to make the Swift standard library more expressive with no boilerplate. https://www.pointfree.co/episodes/ep8-getters-and-key-paths SE-0249: Key Path Expressions as Functions Stephen Celis & Greg Titus • Mar 19, 2019 A proposal has been accepted in the Swift evolution process that would allow key paths to be automatically promoted to getter functions. This would allow using key paths in much the same way you would use functions, but perhaps more succinctly: users.map(\.name) . https://forums.swift.org/t/se-0249-key-path-expressions-as-functions/21780 How Do I Write If Case Let in Swift? Zoë Smith This site is a cheat sheet for if case let syntax in Swift, which can be seriously complicated. http://goshdarnifcaseletsyntax.com Downloads Sample code 0052-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 .