EP 51 · Standalone · Mar 25, 2019 ·Members

Video #51: Structs 🤝 Enums

smart_display

Loading stream…

Video #51: Structs 🤝 Enums

Episode: Video #51 Date: Mar 25, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep51-structs-enums

Episode thumbnail

Description

Name a more iconic duo… We’ll wait. Structs and enums go together like peanut butter and jelly, or multiplication and addition. One’s no more important than the other: they’re completely complementary. This week we’ll explore how features on one may surprisingly manifest themselves on the other.

Video

Cloudflare Stream video ID: 4b4740fd2416aed0c459b85a21325e3e Local file: video_51_structs-enums.mp4 *(download with --video 51)*

References

Transcript

0:05

We have talked about algebraic data types a number of times on this series, and we feel it helps wipe away some of the complexity in the language because we can see the simple math that lies underneath. For example, we saw that structs in Swift behave a lot like multiplication does from algebra, and that enums behave a lot like addition. And knowing that we could refactor our data types to simplify them without changing their intrinsic structure, or we could refactor them to remove invalid states.

0:40

We want to take this idea a step further. We want to say that the connection between Swift’s data types and algebra’s operations is so foundational that we should be emboldened to transfer concepts that are currently defined only on structs over to enums, and vice versa. This will help us see that there are a lot of lots of helpful operations on enums that Swift could be providing to us, but isn’t yet. But maybe someday!

1:20

So, let’s get started. We want to begin with a quick refresher on algebraic data types. We still highly recommend people watch our previous episodes ( part 1 🆓 , part 2 , part 3 ) to get a serious, in-depth look, but this refresher will at least make sure we are all on the same page when it comes to algebraic data types. An algebraic data types refresher

1:44

Algebraic data types tells us that there is a correspondence between the Swift type system and the plain old algebra that we learned in middle school. In particular, it says that structs in Swift are in some sense like the multiplication of types, and enums are kind of like addition of types.

2:01

To make this concrete, consider the following generic type: struct Pair<A, B> { let first: A let second: B }

2:04

It simply creates a pair of values, one from each of the generics provided. The reason this struct is related to multiplication is because the number of values this struct holds is equal to the number of values A multiplied by the number of values B holds. To see this, we can plug in a few simple types for A and B .

2:28

For example, what are all the values in Pair<Void, Void> ? Pair<Void, Void>(first: (), second: ()) We needed to provide a Void value for the first field and a Void value for the second. There’s only a single Void value in Swift, so there’s only a single value in Pair<Void, Void> . This is because 1 * 1 = 1 , which already shows an example of multiplication in the type system.

3:02

What about Pair<Bool, Void> ? Pair<Bool, Void>(first: true, second: ()) Pair<Bool, Void>(first: false, second: ()) Bool has two values, so we can plug either true or false into first , but that makes up all of the values in Pair<Bool, Void> : 2 * 1 = 2 .

3:22

And what about Pair<Bool, Bool> ? Pair<Bool, Bool>(first: true, second: true) Pair<Bool, Bool>(first: true, second: false) Pair<Bool, Bool>(first: false, second: true) Pair<Bool, Bool>(first: false, second: false)

3:39

Now we have four values, and these are the only four values that sit in Pair<Bool, Bool> : 2 * 2 = 4 .

3:44

What about an enum with three cases? enum Three { case one, two, three } What are all the different ways to pair Bool with Three ? Pair<Bool, Three>(first: true, second: .one) Pair<Bool, Three>(first: false, second: .one) Pair<Bool, Three>(first: true, second: .two) Pair<Bool, Three>(first: false, second: .two) Pair<Bool, Three>(first: true, second: .three) Pair<Bool, Three>(first: false, second: .three)

4:14

There are six values, and the only six values that sit in Pair<Bool, Three> : 2 * 3 = 6 . For all of these examples we see that structs are like the multiplication of their types.

4:41

What if we try to pair Bool with Never , the “uninhabited” type, which is an enum with zero cases and a type with zero values. Pair<Bool, Never>(first: true, second: ???)

5:06

While we can provide a first value, we cannot provide a second , which checks out: 2 * 0 = 0 .

5:20

The fact that Pair corresponds to multiplication is reasonable because you must pick a value for first and second , which means for each value in A you must also choose a value in B to pair with it, thus the counts multiply. This is why people sometimes call structs “product” types.

5:41

On the other hand, enums in Swift are kind of like addition. To make this concrete, consider the type: enum Either<A, B> { case left(A) case right(B) }

5:54

It allows you to express the idea of choosing a value from either A or B . It has the proper that the number of values it holds is equal to the number of values in A plus the number of values in B . To see this, we can plug in a few simple types for A and B :

6:10

Let’s start with Either<Void, Void> : Either<Void, Void>.left(()) Either<Void, Void>.right(()) This time we have two cases because we can either plug the single Void value into the left side, or plug the single Void value into the right side: 1 + 1 = 2 .

6:33

What about Either<Bool, Void> ? Either<Bool, Void>.left(true) Either<Bool, Void>.left(false) Either<Bool, Void>.right(()) This time we have three cases: we can plug two values into left or plug one value into right : 2 + 1 = 3 .

6:51

And Either<Bool, Bool> : Either<Bool, Bool>.left(true) Either<Bool, Bool>.left(false) Either<Bool, Bool>.right(true) Either<Bool, Bool>.right(false) Gives us the opportunity to plug the two Bool values into either the left or right side: 2 + 2 = 4

7:04

And consider Either<Bool, Three> : Either<Bool, Three>.left(true) Either<Bool, Three>.left(false) Either<Bool, Three>.right(.one) Either<Bool, Three>.right(.two) Either<Bool, Three>.right(.three) We see 2 + 3 = 5 .

7:24

Finally, what do we get with Either<Bool, Never> . Either<Bool, Never>.left(true) Either<Bool, Never>.left(false) Either<Bool, Never>.right(???) This time we can create values by plugging Bool into the left , but we can’t plug any value into the right because no values exist in Never : 2 + 0 = 2 .

7:47

Here we’ve seen multiple instances of where the values of types adds when forming an Either . The fact that this is true is reasonable because you must choose to either construct a value in A or a value in B , thus the counts add. This is why people sometimes call enums “sum” types.

8:14

Knowing this basic fact is very powerful, because it means we can take complex data structures, transfer them over to the world of multiplication and addition, do some simple algebra to simplify or better understand what is going on, and then translate that back to the world of types and get something understandable back. Again we highly recommend you go watch our previous episodes on algebraic data types ( part 1 🆓 , part 2 , part 3 ) to see lots of examples of applying this idea to real world code .

8:56

The really cool thing about understanding sum and product types is that it shows just how deeply connected structs and enums are. One is not more important than the other for all the same reasons that structs are not more important than enums, or vice versa. When we learned algebra as kids we were not taught that multiplication is more important than addition. Or can you imagine being taught algebra where you are only allowed to use multiplication and not addition? That is of course seems preposterous, but it’s equally preposterous for us to use only structs and no enums, or for us to nice define things on structs without considering what they mean for enums. They are two complementary concepts.

9:33

Given that, we should hope that whenever we come up with generic ideas or operations or constructions involving structs, there is a very good possibility that there is a corresponding version of that for enums. Swift does this sometimes, but not always. Initializers

9:53

As an example of this principle in practice, consider initializers for data types. Structs automatically get an initializer synthesized for them by the compiler, and the initializer is basically a glorified function. struct User { let id: Int let isAdmin: Bool let name: String }

10:13

Here we have a User struct with a few fields. The Swift compiler automatically synthesizes and initializer function for us: User.init // (Int, Bool, String) -> User

10:38

This allows us to do all types of fun things by treating initializers as function. We can feed initializers into map and other higher order functions: [1, 2, 3, 4].map(String.init) // ["1", "2", "3", "4"] ["1", "2", "blob", "3"].compactMap(Int.init) // [1, 2, 3]

11:19

It’s really nice that structs are given this affordance for free, and it’s really nice to utilize these initializer functions with higher-order methods, so what’s the story for enums?

11:30

Turns out, enums also get this special treatment! Each case of an enum is an initializer, and those initializers are again just glorified functions: Either<Int, String>.left // (Int) -> Either<Int, String> Either<Int, String>.right // (String) -> Either<Int, String>

11:58

We can even use them in place of functions: [1, 2, 3, 4].map(Either<Int, String>.left) // [{left 1}, {left 2}, {left 3}, {left 4}]

12:10

The discovery that struct initializers stand alone usually comes before the discovery that enum cases are initializers in their own right and stand alone just as well.

12:25

And just by asking ourselves what it means to be an initializer for structs we can come across this behavior in enums. Even better we see that neither is more important than the other: Swift treats struct initializers and enum case functions equally. Exhaustive compiler checks

12:36

There’s another example that shows how often structs and enums have complimentary feature sets. One of the nice things about enums is that we can exhaustively switch over them. That is, one way of getting access to the data on the inside of an enum, we switch over the cases of the enum, and the compiler forces us to consider all the cases before we are allowed to compile: let value = Either<Int, String>.left(42) switch value { case let .left(left): print(left) // 42 case let .right(right): print(right) }

13:36

This is a wonderful feature because we can build up really complex enums, and by switching on values it’s like having a conversation with the compiler where we are asked to provide actions to execute for each case. It helps us make sure we are considering all the possible edge cases of our programs.

13:52

So Swift provides this really nice affordance of an exhaustivity check on sum types, what about product types? There is in fact, but it actually only works for tuples.

14:04

Let’s consider the following compute function, which returns a tuple of computations. func compute(_ xs: [Int]) -> (product: Int, sum: Int) { var product = 1 var sum = 0 xs.forEach { x in product *= x sum += x } return (product, sum) }

14:50

When we call it, we see our results: compute([1, 2, 3, 4, 5]) // (product: 120, sum: 15)

14:59

We can bind it to a variable: let result = compute([1, 2, 3, 4, 5]) And later access the product or the sum : result.product // 120 result.sum // 15

15:10

Swift also offers the ability to “destructure” tuples into multiple bound variables: let (product, sum) = compute([1, 2, 3, 4, 5])

15:22

And now we can access the product or the sum directly: product // 120 sum // 15

15:29

What’s interesting about destructuring is that it forces us to consider each value in the tuple.

15:37

This means that if we refactor compute to provide an additional computation. func compute( _ xs: [Int] ) -> (product: Int, sum: Int, average: Double) { var product = 1 var sum = 0 xs.forEach { x in product *= x sum += x } return (product, sum, Double(sum) / Double(xs.count)) }

15:50

We get a compiler error: let (product, sum) = computation([1, 2, 3, 4]) ‘(product: Int, sum: Int, average: Double)’ is not convertible to ‘(_, _)’, tuples have a different number of elements

15:52

The compiler is forcing us to address this additional element at our call site. let (product, sum, average) = computation([1, 2, 3, 4])

16:05

And this is another example of something really cool, where a familiar concept—the exhaustivity check we know and love on enums—has a corresponding feature on product types—an exhaustivity check on tuples! And Swift provides this feature almost equally, though it’s not available on structs. Anonymous types

17:09

Now let’s consider an example where structs and enums aren’t on such an equal footing, where Swift very clearly favors structs over enums. And if we go back to thinking of structs as multiplication and enums as addition, it seems preposterous that you’d ever favor structs over enums because why is multiplication any more important than addition?

17:30

A really nice thing that Swift offers to us for product types is tuples, and it’s even one of the oft-touted features of Swift. They give us a very lightweight way to create product types: let tuple: (Int, String) = (42, "Blob")

17:50

The type (Int, String) is kinda like an “anonymous struct”, in that it’s a type that holds two fields, an integer and a boolean, but there is no name for it for us to reference.

18:12

This is similar to “anonymous functions” like this: [1, 2, 3].map { $0 + 1 } That closure is the function that adds 1 to an integer, but it has no name, it’s just used inline right in this map .

18:33

Swift also provides special syntax for accessing the fields in this tuple: tuple.0 // 42 tuple.1 // "Blob"

18:44

And even cooler, we can name these fields: let tuple = (id: 42, name: "Blob") And access these fields by name, instead. tuple.id // 42 tuple.name // "Blob"

19:00

Even though tuples are anonymous, they are given many of the same affordances that structs have.

19:06

Tuples are great because of how lightweight they are. No need to go define a dedicated struct just to hold a bit of data. For example, we could take that earlier compute function, where we used forEach to accumulate a sum and product . We could use reduce instead by using a tuple: [1, 2, 3, 4, 5].reduce((product: 1, sum: 0)) { accum, x in (accum.product * x, accum.sum + x) } // (product: 120, sum: 15)

20:02

We got the exact same result and did the exact same work, but we’re packing a much bigger, expressive punch in a fewer number of lines and statements. We didn’t need the ceremony of creating a dedicated struct.

20:23

Now this isn’t to say that tuples don’t have their downsides, because they definitely do. Tuples can get a little harder to understand the farther they travel. We feel they are best used locally where one can easily see how the tuple is constructed and how they tuple is used all in one place. If you pass around a tuple around a lot, and it travels far from where it was created, it can be difficult to remember what the data actually represents.

20:51

So, tuples are nice and definitely fill a need in Swift. But what does the corresponding story look like for enums? Sadly, there is no story at all. Swift has no concept “anonymous enums”. For enums to not have this feature means that enums are severely disadvantaged when compared to structs.

21:39

In the algebra world, we’re so used to multiplying and adding using operators, which is kind of like the anonymous representation of a product or a sum: 3 * 4 // 12 3 + 4 // 7 With Swift giving dedicated syntax to anonymous product types but not giving dedicated syntax to anonymous enum types, it’s similar to saying that addition is allowed, but you aren’t allowed to use the + symbol. //3 + 4

21:55

Instead you must give a custom name to the operator every time you want to add things: 3 addThreeToFour 4

22:29

So, although Swift doesn’t have anonymous enums today (but hopefully someday), we can at least theorize what they might look like.

22:41

In tuples we used parens and commas to make a new product type: let _: (Int, String)

22:52

So for anonymous enums we’ll use parens and a vertical bar to signify that we are making a choice between types: let _: (Int | String)

23:00

This syntax isn’t going to compile, but let’s pretend it is and consider how we might use this value.

23:06

First, what does it look like to bind a value? let choice: (Int | String)

23:12

Maybe we can reuse the .0 / .1 syntax from tuples: let choice: (Int | String) = .0(42) let choice: (Int | String) = .1("Blob")

23:35

Even cooler, what if we could introduce labels like we had for tuples: let choice: (id: Int | param: String) choice = .id(42) choice = .param("Blob")

23:58

Once we have an anonymous enum value, we will of course want to switch on it. That could be done using the .0 / .1 syntax: switch choice { case let .0(id): print(id) case let .1(param): print(param) }

24:37

Or even the labels: switch choice { case let .id(id): print(id) case let .param(param): print(param) }

24:49

So now that we’ve theorized a syntax that would potentially make anonymous sum types possible in Swift, what would we ever use it for? Well, let’s consider the Optional type: enum Optional<A> { case some(A) case none }

25:08

We all know that optionals are very powerful: it’s a static guarantee that you have to treat the two cases of either having data or not.

25:18

However, a lot of time people find that the none case does not hold enough information as they’d like. Because none could mean a number of things: for instance, it could either mean that some data hasn’t yet loaded, or that there is no data to load.

25:35

People turn to lots of little helper enums to encode this information. enum Loading<A> { case some(A) case loading } enum EmptyCase<A> { case some(A) case emptyState }

25:55

And you might even compose these enums together. Loading<EmptyCase<[User]>> This expresses loading user data that distinguishes between still loading and having an empty case.

26:17

Having anonymous sum types would mean no longer having to build these ad hoc enums everywhere. The Loading type’s name isn’t carrying a lot of weight: the important part is that it has a loading case.

26:35

This means you could write a render function for a view controller that accepts some loadable data to render. func render(data: (user: [User] | empty: () | loading: ())) This expresses a function that can render an array of loaded users or an empty state or a loading state.

27:29

Then in this function we could exhaustively switch over this anonymous data to render it. func render(data: (user: [User] | empty: () | loading: ())) { switch data { } }

27:36

While we locked empty and loading to Void because they don’t carry any additional data, we might theorize an omission of the type to correspond to enum cases with no associated value. func render(data: (user: [User] | empty | loading)) { And this just shows how useful this lightweight syntax would be as an addition to Swift.

28:22

This now brings enums up to the same level as tuples, and these anonymous enums would be useful in all the same ways that tuples are useful. One is not more important than the other, they both just different, complementary concepts. And we’d probably reach for these anonymous sum types all the time if they were available to us, but without the feature, we barely even think of it.

28:46

However, by considering structs and enums by their algebraic properties we can see the usefulness of such a construction. What’s the point?

29:14

This all sounds great, but this is one of those times where we really should ask: “what’s the point!?” We just theorized a syntax that doesn’t even exist in Swift so we can hardly take advantage of it! Why did we spend so much time on something we can’t even use?

29:32

By continuing to understand how algebra is connected to the type system we will continue to get stronger and stronger machinery to definitively say what’s important and what’s not. In this case we can definitively say that anonymous sum types are important, and maybe that’s what can lead to us getting them some day. And the more that are aware that such a feature should exist, the more likely they will be able to influence the direction of the language through something like Swift Evolution.

30:04

While none of that is applicable to everyday programming, what is is the process of seeing something on product types and being able to ask: is there a complementary construction on sum types—and vice versa!

30:22

And next week we’ll look at a very real-world example of how Swift prefers structs over enums, but in this case we’re going to be able to address the problem ourselves and give enums some superpowers. References Algebraic Data Types Brandon Williams & Stephen Celis • Feb 19, 2018 Our introductory episode on algebraic data types. We introduce how structs and enums are like multiplication and addition, and we explore how this correspondence allows us to refactor our data types, simplify our code, and eliminate impossible states at compile time. Note What does the Swift type system have to do with algebra? A lot! We’ll begin to explore this correspondence and see how it can help us create type-safe data structures that can catch runtime errors at compile time. https://www.pointfree.co/episodes/ep4-algebraic-data-types TypeScript: Advanced Types TypeScript has a feature closely related to enums and anonymous sum types. It is called “union types”, and it also allows you to express the idea of choosing between many different types. https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types TypeScript 2.0: Tagged Union Types Marius Schulz • Nov 3, 2016 TypeScript’s type system is also flexible enough to emulate’s Swift’s enums, but anonymously! Swift enums are sometimes called “variants” or “tagged unions” because each case is “tagged” with a label: for instance, the Optional type is tagged with some and none . https://mariusschulz.com/blog/typescript-2-0-tagged-union-types Null-tracking, or the difference between union and sum types Waleed Khan • Jul 24, 2017 An in-depth look at the differences between sum types and union types. https://waleedkhan.name/blog/union-vs-sum-types/ Polymorphic vs. ordinary variants in ocaml Yehonathan Sharvit • Mar 16, 2018 OCaml supports anonymous sum types in the form of “polymorphic variants,” which can describe a single, tagged case at a time. https://blog.klipse.tech/ocaml/2018/03/16/ocaml-polymorphic-types.html Tuple : Struct :: ? : Enum Matt Diephouse • Jun 22, 2017 A Swift community Twitter thread about anonymous sum types. https://twitter.com/mdiep/status/877936255920619521 Downloads Sample code 0051-structs-🤝-enums 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 .