Swift’s Reflective Underpinnings - Joe Groff
Joe Groff at Swift Summit 2017
I'm Joe Groff. I'm a member of the Swift Core team and I work at Apple on the Swift Implementation and I'm here today to talk to you all about Reflection. That might not be the first thing you think about when you think about Swift, and you may have heard that it's something that Swift doesn't care about or that it’s impossible in Swift.
When most people think about it, they probably think about Swift's type system. And particularly the type checker. After all, that's the first thing you interact with when you're writing code as it critiques your code. Now, whether you see that as a useful tool that gives you useful advice or as a tyrant with insatiable demands. You might think that that's all there is to Swift’s type system. That it's just an obstacle to overcome at compile time and forget about.
But there's a secret double life to Swift’s type system at runtime. Even after the compiler is done, a Swift program has access to a lot of meta data about the type system and other entities that were in the program. And we can harness that meta data to implement Reflection features. And write code about our code. Look at the types and the methods and other objects that we defined in our code and build higher abstractions on top of that.
This is the bread and butter of working with dynamic languages like Objective-C and Ruby. And it's something that has an important part in Swift's future as well. I'm going to look at a bunch of problems that are, I think, are solved well with Reflection and I'm going to look at how Swift's runtime could be used to solve these problems.
Some of these are things that you can do with Swift the language today. And some of them need a little bit of additional language or library design to reveal. I’m also gonna look at how Reflection can benefit not only a finished running program, but the entire development process of building Swift. When I have to take the time machine to look to future Swift, I'm gonna use this icon on a slide to indicate that this isn't something you can write in Swift today.
Keep in mind though, that this is all not planned of record, this is just what I think is important. But I hope that by sharing the functionality that exists in the runtime, the potential that's there, and some of my ideas of how that potential can be harnessed, we can start a dialogue and structure the process of making these into real first-class features in Swift.
People tend to think of static typing and Reflection as being diametrically opposed. But ultimately, I think we can come up with designs that marry the best aspects of both. Providing the expressivity and power of Reflection while still preserving the static reasoning capabilities of the type system. So with that, let's get started.
And what better topic to start with then everyone's favorite topic; object oriented programming. It can be hard to remember just how much of a revelation object oriented programming was when it came into the industry two or three decades ago. It was the first paradigm to really get people thinking about building extensible, composable, reusable systems. And it brought in the architecture of the possibilities that people would even consider building in software. And it's been core to the success and longevity of Apple's own frameworks.
In a lot of ways, type languages are still trying to catch up with those first object oriented languages and their ability to build extensible systems. Swift's type system has some cutting edge features to allow you to build extensible systems in certain ways. For instance, if your components can agree on a common set of operations, you can express those as a protocol. And then modules can freely add new types that conform to those protocols and extend the system.
Now as long as you talk to that protocol, the system remains open and decoupled. However, you can't add new methods to the protocol without breaking your existing conformers. If you wanna build a system that's open to new operations, you could potentially use an enum instead. Closing over a close set of types as associated objects of that enum. And this allows other modules to add new operations freely to that enum by switching over that thick set of cases.
The trade off you've made here is that now, of course, you can't add new types. So what if we wanna build a system where we can extend both directions? We wanna be able to add new operations and new types at the same time. This creates an interesting problem because not every type necessarily knows about every method. And modules may add methods that only make sense for their types and may not have a sensible implementation for another module's types.
So we no longer have a single type entity in the type system that can capture this system accurately. We need a more richer, more complex dialogue between the types and the operations to figure out what to do. And this challenge of building an extensible system that's open to both new operations and new types is called the Expression Problem. And object oriented languages have a lot of great solutions for solving this problem.
Let's look at a concrete example. The Expression Problem comes up a lot in UI design. And particularly the problem of event routing. A traditional desktop system has a lot of shared physical devices like keyboards, mice, tablets, musical instruments as well as shared on screen things like the doc and the menu bar. And these send actions that can be handled by an open set of controls, things like text boxes and buttons that may have focus and be expected to respond to any of these actions.
Ideally we wanna keep these two sides of the system decoupled. We outta be able to add new menu items or plug-in new devices without updating and of the control code. Likewise, I wanna be able to introduce new kinds of controls without having to handle those controls in all of our existing event handling code. A lot of you are probably cocoa developers and know where this is going. AppKit has a really great solution to this called the Responder Chain.
They realized that while a single control may have focus, it's part of a larger system. That control belongs to a window, which belongs to a document, which belongs to an app. And you can walk up that chain to find a responder that makes sense for almost any action in the system. So if we send an action that makes sense for a text box, something like pasting or copying or entering text, then we do the obvious thing and let the text box handle that.
But if we send in action that doesn't make sense for a text box to handle like closing a window, that's still fine. We don't have to crash the program- We just walk up the responder chain and find a parent that knows how to handle and respond to that action. Now what makes the responder chain natural and obvious in Cocoa is the ways Objective-C works. It's a fairly classic implementation of message passing, which under all the orbs and UML diagrams is really the core of what object oriented programming is all about.
If you have an object, the main thing you do is send messages to it. And the object decides what to do with that message. We get to extend the system by introducing new objects that respond to the same messages in different ways. What gets more interesting is that the message names themselves are first class values- in Objective-C they are called selectors, and that lets us ask higher level questions about the object.
We can send messages that ask, "Do you even respond to this method? And if so, could you please do what you would do in response to it." Even if that method isn't hard coded in the source. And these are the raw materials that allow the responder chain to be implemented in Objective-C. At an implementation level, what powers this is the way objects are constructed in Objective-C. Every object has a pointer to its class object and among other meta data, that class object has the method table, which describes all of the methods that class can respond to and their implementations.
And it makes that information available to the program for reflective queries, things like respondsToSelector. So, if someone went back in time and made it so Objective-C didn't exist, would we be able to implement the responder chain in Swift? It may not be obvious, but the answer is yes. Although an arbitrary Swift value type, something like a struct or an enum, doesn't have a fixed representation unlike Objective-C, we do have a type meta data object for every type in a Swift program. And we can use this type meta data object at runtime, paired with a value of the type, to create a dynamic value that we can manipulate at runtime. And this is what composes Swift's Any type. This combination of a type meta data object and a value of that type. Given an Any value, even though the type meta data doesn't directly contain a method table unlike Objective-C, there's still a table of all the protocol conformances in a system kept by the Swift runtime.
When we cast to a protocol type, the runtime consults this table and searches for the implementation of that protocol if there is one. So using this technique, we can ask the same questions that we asked in Objective-C. "Do you respond to this method and if so, please respond." And we can factor this out into a generic function, allowing us to avoid hard coding a particular responder protocol and use any protocol in our system as an action for responders to respond to.
So although the elements are less tightly coupled in Swift than they are in Objective-C, the analogies are there. You can use type meta data and protocols the same way you use classes and selectors in Objective-C. Now I'll admit that while the runtime is willing, the language design here is still a little bit weak. It takes a little bit of lateral thinking compared to what you would do in Objective-C to realize this is even possible.
And you can say that defining a protocol for every single message you wanna deal with dynamic is a little heavy weight. Non the less, there's some good things about this design too. The fact that type meta data is decoupled from any fixed representation of a type means that we can use it with anything in a Swift program. Everything from Bools and Ints, all the way up to user defined types. And the language interface is still type safe ultimately.
We have to ask the question, "Do you conform to this protocol," before we can use any methods on that protocol. And because it produces an optional, we also have to think about the case that it may not respond to that protocol. In Objective-C, you don't even have to ask the question. You can just send arbitrary messages and hope for the best. So that's a fairly dynamic reflective technique that you can do in Swift today.
Now I'm gonna look at some more interesting problems that Swift doesn't really have a good answer to today, though the runtime functionality and potential exist. One of these is the problem of configuring and adapting your programs behavior at runtime using non-code assets. For example, if you're deploying a web backend. Your administrators probably wanna be able to do V-host and URL routing without hacking on your code.
If you're building a game. You want your game designers to be able to layout there maps, put in objects and scripts and stuff without hacking on your engine. Some people like to use interface builder to build there UI's in Cocoa and on iOS. And there's a lot of good reasons that people wanna do this. It can make changing certain things a lot faster than having to go through a compile-and-let-it- run cycle. It can help stratify your architecture shielding people from the details of your engine implementation while allowing them to script higher level aspects of the system.
And also allows the program to adapt more readily to user settings or changing server environments. Although these resources, these configuration files, nibs, story boards, and so on aren't code, they're still part of your app. And ideally you'd be able to just reference elements of your source code directly in these resources. What you'd like to be able to do in Swift is ask the runtime. Give me the type with this name as part of your general parsing code.
And then given that type, ask additional questions like, "Does it conform to a certain protocol?" Of course, there's no public API for this in Swift today. So you may have found yourself having to write manual registration tables, something like this. Now there's nothing good about this. It's boiler plate, it's easy to make typos, it's easy to forget to add entries if you add new types. So it's really something that the language ought to make easy and handle for you.
The good news is that the runtime is fairly well set up to support this kind of query. Even though the implementation isn't yet there in the standard library. Those type meta data objects that I talked about for Structs, Enums, and Classes, have a reference to what's called the nominal type descriptor. And this contains fairly detailed information about the original definition of the type. It includes its name, it includes the module or parent type it was nested in, and it includes a fairly detailed account of its layout as well.
This is all the information we need in order to ask that question. Give me the type of that name. The subscriptors are helpfully gathered together in one section of the binary and they're mapped together in memory. So if someone wanted to implement this, they could write code that scaned through that section, found a nominal type descriptor, and then used that to re-instantiate the meta data. Once we have the meta data, we can then use the protocol conformance table to find methods on that type.
And this would allow us to handle these sort of runtime configuration problems elegantly and easily in Swift. Although the runtime already has good support for this sort of type by name look up. It doesn't necessarily have the fine grain meta data for methods or properties yet, which would be interesting for something like interface builder that allows really fine grained control of the runtime behavior.
Non the less, we're fairly close to being able to support something like this. And I think I'd be a valuable addition to the language. Now registration tables are just one example of a more general problem of boiler plate. And Reflection's a great general solution to the problem of boiler plate. There's a lot of code that's so obvious that a computer could write it. And Reflection is all about writing code about your code.
You probably had to write a lot of code like this if you've written any amount of Swift. If you had the right initializer for your classes, write an equality operator for your user defined types, or pulled out type data from JSON or from other dynamically typed data stores. Then you probably wanted to... probably cried out for a better answer. There's nothing good about this. Again, this is boiler plate, it's boring code to write. It's easy to copy and paste and make mistakes.
In fact, there's one hidden in this slide. And for things like equality and serialization especially, it's ... the type system won't help you maintain this if you add new fields to your types. So it's important not only for developer happiness, but for correctness and robustness that the language had better solutions to these problems. And then people have observed that static code generation is a good solution to some of these problems in addition to runtime Reflection.
You could think of code generation as static Reflection. You're writing code about code, but you're doing it with compile time information rather than at runtime. The language itself has grown a lot of one off features to target the most egregious offenses in the boiler plate category. Things like Elementwise Initializers and the Codeable, Equatable, and Hashable protocols, now have automatic deriving rules built into the language.
This is great, but it's not a general solution to the boiler plate problem. So some people have turned to external tools. Sourcery and SwiftGen are some popular ones from the community and even the standard library has succumbed and written their own tool called gyb to generate their boiler plate. It seems inevitable to me that we'd wanna have a first class macro feature for Swift to integrate this.
And there's a lot of good things to say about code generation as well. The code that gets generated still has to make it through the compiler. So you have some guarantee that it makes sense with the rest of your program, that it's type safe, and that it meets access control and goes through well defined interfaces in your code. Generating code for every situation you needed can lead to faster code in some cases because you have a copy of that code that's specialized for the situation it's used in.
That's not always what you want though. For things like Equatable and Hashable, it may be obvious that you want those to be fast, but for something like serialization, it's less clear to me. Maybe you are IO bound and you don't need the performance and you're code is already big and you don't wanna pay for that code size cost. Code generators like SwiftGen and Sourcery can complicate your build process if you have to integrate them into your build system.
So it really pays to have first class language support for these things. One of the more fundamental problems with code generation is that you have to think about using it ahead of time. You can't access it dynamically at runtime if you need functionality for de-bugging or logging or other purposes you didn't anticipate if you didn't use it at compile time. So I don't know that code generation is the only solution we wanna have for scraping boiler plate in Swift.
And thankfully, I don't think we need to. As I was just saying, there's fairly detailed information in these nominal type descriptors that we could use to provide more interesting runtime Reflection capabilities. Some of you may have played with the Mirror type. This is kind of a proof of concept of the things that are possible using this meta data. It's used to implement printing and Playground logging and LLDB quick looks. And although it's enabled some things, it's still fairly limited.
It doesn't allow you to write to objects or initialize new objects. So you can't use it to implement serialization or database interfacing. And the other annoying thing about it is that you have to have a value of the type in order to use it. It can't give you abstract meta data about the type independent of an instance of the type. So it's badly in need of a replacement. In Swift 4 we introduced KeyPaths and I think this is a promising basis on which to build better, more powerful Reflection capabilities.
KeyPaths act as indexes directly onto a value so they integrate more cleanly into Swifts mutation model for values. With some fairly small additions to the KeyPath API, things like the ability to get KeyPaths for all the fields of a type, as for a KeyPath by its name, or build a new instance of the type given a set of key valued pairs, we'd have a fairly complete and powerful story for manipulating values dynamically using runtime meta data.
And this would be great because we'd win back some of the things we lost if we relied on code generation. We'd be able to have single shared implementations for things that don't need the performance of specialization. Furthermore, we always have this meta data available at runtime even if we didn't anticipate the need for it at compile time. This is great for things like printing where if you're stuck to print at the bugging, you really don't wanna have to go back to your code and re-compile to figure out what the problem is.
Now the trade offs, in addition to the obvious size versus speed trade off, there's the question of circumventing the safety features of the language. A lot of things like access control don't exist at runtime and thoughtlessly designed API's can very easily circumvent and cause problems. It can also make maintenance more difficult by hiding dependencies from refactoring and other static code manipulation tools.
So I wonder if we can do even better than that. Looking at some of these trade offs occurred to me that these are the same trade offs that you see when we look at implementation models for generics. Swifts type system was carefully designed to support either specialization or runtime dynamic dispatch with the same source code. And no matter what implementation you use, you get type safety from the compiler.
So I wonder whether we could do the same thing for reflecting over the shapes of types. In the functional programming world, languages like Haskel started experimenting with what they call generic deriding. Instead of having special case behavior for a certain protocols, certain core protocols, they instead define a deriving rule for one fundamental shape protocol and allow everything to be built on top of that. We could do something similar in the future for Swift.
We could have something like this protocol for Structs, that lists the shape of the struck into the type system and provide some fundamental methods for building and taking a part Structs into their structural representation. This would be really powerful because we could fully allow the special case behavior we currently have in the language into one implicit internal protocol conformance. And since the Reflection information has been brought into the type system, we can use it to build other type safe abstractions.
We could implement the default equatable implementation safely as a normal protocol extension using constrained protocol extensions. So while this is a very ambitious topic and probably far in Swifts future, I think it would be an interesting way of fusing together the static and dynamic aspects of Swifts type system. It would give us reflective code that could be implemented either way. And still get static guarantees and compiler knowledge that this information is being used.
Overall, this whole topic of boiler plate generation is really interesting and I've mostly focused on the problems of reflecting over the shapes of types because that's one common use case, but there's a whole world of use cases to explore here. And I hope that this starts a discussion about these sorts of problems in Swift. So far, I've been focusing on how Reflection can benefit a finished running Swift program, but Reflection can also play a role in the overall life cycle of a Swift project.
There's a lot of benefit to be had to having this meta data available to external tools. And to that end, Swifts meta data has been designed it can be used externally by non ... by code that doesn't run inside your process. The formats were designed such that they could be read off of disc in the same way that they are mapped in memory in a running process. And you don't need to run any code in the process to be able to interpret most of this meta data.
This is what enables things like Xcodes memory graph debugger, which uses the meta data in a Swift program to walk the entire heap of a running program and present it as a graph. This makes it really easy to figure out if you have object graphs that are a little longer life than you thought they were or if you have cycles or if you have other problematic memory usage issues in your program.
To that end, I think an interesting future direction would be to use this meta data for something more targeted towards detecting, diagnosing, and fixing cycles. While we like ARC and we don't wanna pay for a garbage collector in production code. The implementation techniques of a garbage collector could be used over Swifts meta data to help us find cycles. And more directly diagnose and offer fix its to break those cycles.
Reflection is also an important aspect of the future of debugging Swift. Up until now, LLDB's been very tightly coupled to the Swift compiler and this has caused a lot of problems through no fault of LLDB itself. Swift's compiler is moving very fast, a lot of its data structures change all the time, and it's hard for the debugger to keep up. By having this position independent meta data available, the debugger should be able to become a lot more decoupled from the compiler and be more robust.
It should also enable a larger universe, of more interesting debugging tools in addition to just a general LLDB debugger. So we've only just started scratching the surface of what we can do with Reflection meta data out of a process to make the entire Swift debugging, Swift development system better and easier to use. So if Reflection's so great, why didn't we ship Swift 1.0 with it? Why don't we just implement all these things if they're so easy?
Well I'm an optimist and I think that there's a lot of benefit to be had to Reflection. There's a lot of good reason to be skeptic as well. And I mention some of these previously in the talk. There's a big question about the ability for thoughtless Reflection to undermine API design and reduce the robustness of Swift code. There's the ability ... there's a potential to create maintainability problems if Reflection is used and it breaks the ability for refactoring and other tools to update your code.
There's also the general problem that in order for you to write code using this Reflection information, that information ... like detailed information about your source has to be preserved in a binary. That's not always acceptable to everybody. A lot of people want to keep as many details of there implementation hidden as possible. And while it may be very convenient to be able to load up an arbitrary type by name and call methods on it in your own configuration files, you really don't wanna do that with user data that you loaded off the network.
That'd be a pretty bad security problem. Also fundamentally, while we've done our best to optimize this meta data to use as little size as possible, it's still has a cost. Non the less, I think that there are solutions to these problems both through language design that helps us still leverage the static type system while improving the expressivity of the language, and by providing opt-in mechanisms to allow Reflection meta data to be used where it's needed and allow it to be alighted where it's not ... or where it's not desired.
So I think Reflection has a bright future in Swift. I looked at a number of problems both in process and out of process that are well served by Reflection and I looked at how the tools in the Swift runtime could be used to implement these features. If any of this piqued your interest, I hope that some of these are breadcrumbs that you can use to start looking at the open source Swift runtime and maybe start exploring and try implementing things on your own if you're so interested.
I looked at things at a very high level because I only have so much time on this stage here, but I'll be in the labs after this if you wanna ask me for more details or share more use cases with me or discuss philosophy, whatever you want. One thing I'm really excited about, even though ABI stability is our foremost concern this year, it should eventually lead to the stabilization of these formats so that libraries outside of the standard library, outside of the core Swift project will be able to utilize them and provide functionality that we didn't think of or that we didn't necessarily want to finalize as part of Swift forever.
The flip side of this is that we also wanna stabilize these formats without closing any unnecessary doors. While there will always be the ability for future Swifts to add new meta data, once the ABI's stabilized, that will have backward deployment and compatibility concerns that we don't have today. So we wanna make sure we get things as right as possible ahead of time. To that end, not all contributions are code.
We do pay attention to the internet, we read Twitter, we read blogs. Most of the use cases I put together in this slide deck came from blogs. So we really value hearing feedback about things that don't work well in Swift today and that you wish were more expressive. So that's all I had. Thank you for listening. I'll be in the labs. Thank you.