Swift uses type inference to help you write clean, concise code without compromising type safety. We'll show you how the compiler seeks out clues in your code to solve the type inference puzzle. Discover what happens when the compiler can't come to a solution, and find out how Xcode 12 integrates error tracking to help you understand and fix mistakes at compile time.
Hi, everyone. My name is Holly, and I'm an engineer on the Swift compiler team. Welcome to "Embrace Swift Type Inference." Swift uses type inference extensively to achieve concise syntax without compromising safety in your code. Today, we'll talk about when you can leverage type inference, we'll learn about how type inference works in the compiler, and finally, we'll see how to work with Swift and Xcode to understand and fix compiler errors in your code. Before we dive in, let's review what type inference is. Type inference allows you to omit explicit type annotations and other verbose details in your source code when the compiler can figure out those details from the surrounding context. In this case, there is no type for x written in the source code, but the compiler infers the type of x to be a string based on the value provided in the assignment, which is a string literal.
The type of x could have been specified explicitly with a type annotation using a colon after x or by coercing the right-hand side to String using the "as" keyword. This is a very small example, but things get much more interesting when the code leans heavily on type inference, like in a SwiftUI project. Many SwiftUI apps create large, complex interfaces by composing small, reusable views, and this kind of code relies heavily on type inference at the call-sites of those reusable components. We're going to take an in-depth look at code in a SwiftUI app that leverages type inference at these call-sites. Then we'll learn how the compiler solves the type inference puzzle, and finally, we'll see how to work with the Swift compiler and Xcode to understand and fix compiler errors in the code.
We'll be looking at an app that I've been working on with my team, called Fruta, for ordering smoothies. Currently, the app shows a list of smoothies, which you can see on the right in the Preview.
I want to add a feature to allow the user to search for smoothies.
Let's take a look at how the current smoothie list is implemented.
The SmoothieList body creates a SwiftUI List from an array of smoothies, mapping each smoothie to a SmoothieRowView. To add a search functionality, I need to filter the smoothies based on a string that the user searched for, so SmoothieList needs a State property to store this string.
I built a new reusable view called FilteredList that is similar to List, but allows me to pass two additional arguments to the initializer that specify how to filter the smoothies.
The first is a KeyPath to the property on smoothies that I want to filter by, and the second is a function to compute whether the smoothie should be included in the list based on that property.
In this case, the smoothies will be filtered by their title, and a smoothie will be included if the title has the searchPhrase as a substring.
The hasSubstring method does the work of checking for the searchPhrase within the title, and it will also return true if the searchPhrase is an empty string.
It's not obvious from looking at the call-site, but this call to the FilteredList initializer leans heavily on type inference. Let's talk about what causes this code to rely on type inference, and how the call-site leverages type inference for cleaner code. To understand the incidental details that are omitted from this call-site, we need to look at the declaration of FilteredList and its initializer.
Since FilteredList is a general-purpose view, it should work with any type of data and content that the client needs to show in their list. This flexibility is achieved using generics.
I introduced three type parameters, which are placeholders that will be replaced with actual types at the call-site. These actual types, which are formally called concrete types, are either specified at the call-site or inferred by the compiler. In this case, Element is a placeholder for the type of element in the data array, FilterKey is a placeholder for the type of the specific property on Element that the data will be filtered by, and RowContent is a placeholder for the type of view to be displayed in each row of the list. Now that I have these three type parameters, I can use them in the initializer's parameter list. Each place that a type parameter appears in the initializer's parameter list is an opportunity to leverage type inference at the call-site by providing an argument that gives the compiler a clue about what the type parameter should be replaced with.
We'll see how this works later, but right now, let's focus on the type of each parameter as we build up the list. First, FilteredList will be initialized with the data to map, which is an array of Elements.
The next parameter is a specific property of Element to filter by, which is a KeyPath with an Element base type and a value type of FilterKey.
The next parameter is the isIncluded closure, which is a function type taking FilterKey as input and returning Bool. The closure is marked as "escaping" because FilteredList will need to store this closure in a property.
The last parameter is a closure to map a data element to a view, which is a function type taking Element as input and returning RowContent.
The closure is also marked as "escaping" because it needs to be stored.
Note that this parameter is marked as a ViewBuilder to enable the SwiftUI DSL syntax in the closure argument at the call-site. The SwiftUI DSL enables you to declare several child views by listing them out in the body of the closure, and the ViewBuilder will collect the child views into a tuple for the parent view to work with. And this is the full initializer of FilteredList. When a FilteredList is initialized, its type parameters will be replaced with concrete types. Let's take a look at this initializer side by side with the call-site in SmoothieList to see how the call-site leans on type inference.
Notice how clean the call-site is. There are no explicit type annotations written at the call-site, but it still gives the compiler all of the information it needs to infer concrete types for each type parameter of FilteredList.
Let's see what this call-site would look like if all of the argument types were explicitly specified in the code. We'll use placeholders for type parameters to see exactly where type inference needs to fill in a concrete type.
First, the three type parameters of FilteredList could have been explicitly specified at the call-site in angle brackets after FilteredList.
The type of the first parameter, which is an array of Elements, could have been specified using the "as" keyword after the "smoothies" argument.
In the second parameter, Element is the KeyPath base type. This could have been specified by writing an explicit base type in the KeyPath literal between the backslash and the dot.
The KeyPath type as a whole could have been specified using the "as" keyword after the KeyPath literal.
Next, the type of the isIncluded closure could have been specified using a type annotation in the closure body.
And finally, the type of the RowContent closure could have also been specified using a type annotation in the closure body.
Now, the initial source code that I wrote is riddled with a lot of verbose type annotations, including a bunch of placeholders for types that need to be filled in by type inference.
Instead of figuring out what these placeholders need to be filled in with manually, I am relying on type inference to figure out exactly what those types should be. In other words, type inference helps you write source code faster because you don't need to know exactly how to spell all of these types in your code.
Let's talk about how the compiler figures out what these placeholders need to be replaced with. You can think of type inference like a puzzle. The type inference algorithm solves the puzzle by filling in the missing pieces using clues from the source code.
Filling in one piece of the puzzle can also uncover more clues about the remaining pieces.
Let's see if we can solve this puzzle together, using clues from the source code just like the compiler does for us with type inference. First, the "smoothies" argument could tell us what to fill in for Element, and we know that "smoothies" is a property which already has a type. Using QuickHelp, we can see that the property is an array of "Smoothie" elements, so Element can be filled in with "Smoothie." Now that we have filled in one of the Element placeholders, we can replace all of the other Element placeholders with that same "Smoothie" type.
Now, filling in the other Element placeholders uncovered a clue for the concrete type of FilterKey because now we know that the KeyPath literal is referring to Smoothie.title.
Using QuickHelp again, we can see that Smoothie.title is a string, so FilterKey can be filled in with "String." And now that we know what FilterKey is, the other FilterKey placeholders can also be replaced with "String." The last piece of the puzzle is RowContent, which is the return type of the trailing ViewBuilder closure.
Since this closure only has one view in the body, the ViewBuilder will return the single child view, which has type SmoothieRowView.
And once again, now that we know what RowContent is, the last placeholder can also be replaced with SmoothieRowView. And with that, we've solved the last piece of the puzzle. This is the strategy that the compiler uses to infer types throughout your code. The code that you write provides clues for the compiler to use. And each step of the algorithm uncovers more information for subsequent steps to use. However, it's possible for one of the clues in the source code to cause the compiler to fill in one of the placeholders with a concrete type that doesn't fit with the rest of the puzzle. If one of the pieces doesn't fit and the puzzle can't be solved, it means that there's an error in the source code. Let's talk about how the compiler modifies its type inference strategy to solve the puzzle in the presence of source code errors. Then, we'll take a look at how you can utilize your tools to understand and fix those errors.
Let's rewind back to where the compiler inferred the concrete type for FilterKey. Remember that, in the previous step, the compiler inferred Smoothie as the key-path base type, and it used this information to figure out the concrete type for FilterKey by looking up the type of Smoothie.title.
If I had made the mistake of using the wrong property in the key-path literal, the compiler would have tried to infer the type of FilterKey to be the type of that incorrect property, which in this case is Bool.
Then, it would continue to fill in the other FilterKey placeholders with that same incorrect type.
Now, if we look at how this Bool type is used in the isIncluded closure, it's clear that this piece doesn't fit because Bool doesn't have any method called hasSubstring. So, the compiler needs to report what went wrong.
Making mistakes in the code that we write is inevitable, and programming languages and their tools must be designed to account for our limitations as humans. The Swift compiler is designed to catch these mistakes by integrating error tracking into the type inference algorithm to use later on in error messages. During type inference, the compiler will record information about any errors it encounters. Then, the compiler uses heuristics to fix errors in order to continue type inference.
Once type inference is done, the compiler will report all of the errors it collected, often with actionable fix-its to automatically fix the error in the source code or with notes about concrete types the compiler inferred that might have led to the error. Integrated error tracking was introduced for many error messages in Swift 5.2 in Xcode 11.4, and in Swift 5.3 and Xcode 12, the compiler uses this new strategy for all error messages in expressions. Now, we have all had frustrating moments when trying to fix compiler errors in our code. But error messages are designed to be like a pair programmer that is built into the compiler to catch your mistakes and help you fix them, rather than allowing you to continue on with that error in your code.
Let's jump over to Xcode to see how to work with these tools to fix errors while writing Swift code. Before writing any code, I'm going to open up the Xcode menu under behaviors, and click on Edit Behaviors.
I'm going to add a behavior for when the build fails to automatically show me the issue navigator. Now, I will see all of the errors across my project each time my project fails to build. Here, were looking at the current implementation of SmoothieList and its preview. I've already added FilteredList to my project which you can see in the project navigator. But before I replace List with FilteredList, I need to add a search field above the list. I've already added the State property to store the user's search phrase. So now, I'm going to add a TextField above the list in the VStack.
I want the title of the text field to be "Search" and I'll pass in the search phrase. And now I'm going to build using Command+B to make sure I haven't made any mistakes.
Notice that there is now a compiler error on the line of code that I just added.
I can expand the error by clicking on it.
And it looks like I used an argument whose type, which is "String," isn't compatible with the expected parameter type of this TextField initializer, which is binding. I made the mistake of passing the search field value rather than its binding and the Swift compiler was able to figure out that the binding does have a compatible type, providing me with a fix-it to refer to the binding instead, using dollar sign.
Next, I want replace this List with my new FilteredList.
And I'm going to build again to make sure I haven't made any mistakes.
But it looks like there's another error. If I expand the error, I can see it's about Smoothie not conforming to the identifiable requirement of FilteredList. And this might be a little confusing because "Identifiable" isn't written anywhere in this part of the code.
Notice that on the left, in the issue navigator, there is a compiler note attached to this error.
With the new integrated error tracking, the compiler records information about what was happening during type inference when this error was encountered, which allows the compiler to leave breadcrumbs that direct you to look at other parts of your code through compiler notes. These notes can help you connect the dots between the error that you see in your source editor and essential information from other files in your project. And you'll be seeing a lot more notes in Swift 5.3 and Xcode 12.
I want to view this note side by side with the error, so I'm going to close the canvas, which I can do using the shortcut cmd+enter. Now, I'm going to hold down Option and Shift while clicking on the note to show the destination chooser.
And now I can arrow over to the right and hit Enter to open the file in a new editor.
Now, we are looking at the FilteredList declaration. And if I expand the note, it points out that the compiler inferred the concrete type of Element to be Smoothie.
At this declaration, I can see that Element or Smoothie is required to conform to "Identifiable." I must have forgotten to add this conformance to Smoothie before trying to use it in FilteredList. In the editor on the left, I'm going to cmd+click on Smoothie, and use Jump to Definition. And here I can add the required conformance to Identifiable. Smoothie already has a property called "id." So the requirements of the identifiable protocol are already implemented. But just to double-check, I'm going to build one more time to make sure that this does fix the error.
And it looks like we're error free again. Now, let's close the editor on the right, and hit the Back button to go back to SmoothieList. Now, if I use QuickHelp on one of the arguments to the FilteredList initializer, I will be able to see the concrete type that was inferred by the compiler. Let's try using QuickHelp on the key-path literal using option+click.
Just like we saw when solving the type inference puzzle, the compiler inferred the type of title to be a string based on the inferred key-path base type. And remember that the compiler inferred the base type to be Smoothie based on the Smoothie's argument on the line above.
Before we finish up, I wanna make sure the filtering works using the canvas. I can turn the canvas back on using the key binding cmd+option+enter.
And I'm going to add a another preview in the preview provider that has some text in the search bar.
Now, the preview provider is iterating over all of the search phrases that I want to test, which in this case are empty string and the string "Berry." And if I refresh the preview, using cmd+option+P, I can see that it works. I can see the second preview with "Berry" typed into the search bar and only the Smoothies with Berry in their title are showing in the list.
When I made typos and other mistakes in my code, error messages from the compiler were actionable and informative. Furthermore, the new integrated error tracking in the compiler allows the compiler to gather a lot more information about failures, which the compiler presents through notes.
These are just a few of the improvements that you'll find using Swift 5.3 in Xcode 12. During this session, we learned that SwiftUI code relies heavily on type inference when interfaces are composed of reusable views. Next, we saw how type inference fills in incidental details that are omitted, by figuring out those details using clues in the source code. Finally, we learned about how the compiler integrates error tracking into the type inference algorithm to record more information and leave breadcrumbs for error messages which help you understand and fix mistakes. To learn more, you can read the Swift blog post about the compiler's new integrated error tracking called the new diagnostics architecture.
To learn more about building SwiftUI views, I recommend following a SwiftUI tutorial on developer.apple.com. Finally, check out Swift Generics from WWDC 2018 for an in-depth look at generics in Swift.