Dynamic Swift

Chris Eidhof at Swift Summit San Francisco, 2016

swiftsummit


Transcript:

Chris: Hey everyone. It's such a nice, big stage. I sort of want to run around, and you know, go back and forth. Oh, sorry. But I'm going to be doing ... That's my background picture. I'm going to be doing live coding, again, so I'll have to stay at my computer. So, live coding is always a little bit scary. I practiced a lot, of course, but I might do something wrong. If you see me make a mistake, please shout out. With that said, let's get started.

The title of my talk is Dynamic Swift. Dynamic programming means a lot of different things to different people. I think, in the iOS community, and the Cocoa community, and the Swift community, people usually, when they say dynamic, they mean runtime programming. We're going to take a runtime API, and replicate it in Swift, and see how Swift actually is a very dynamic language, even if it doesn't have that much support for runtime programming yet, it might not need too much of it.

So, over here, you see this Person struct, with a couple of properties, and then I have some computer scientists in an array, and we're going to try to sort that array. Let's see. My mouse is a bit tiny. We can do something like this. There's two methods to sort an array. The first one is sorted by, and it returns a new array, so it's an immutable version, and then there's the in-place sort, which sorts an array in place. Both of them take a function, two person values, and they need to return a bool in that function. The bool has to be true if the two values are in ascending order.

To sort, it calls the function maybe twice. First, if it returns true, they're ascending, then it calls it again, with the arguments flipped. That means, if it returns true, they're descending, and otherwise, if it returns false twice, then they're the same. This is a nice way to sort things, but if we look at NSArray, so if we write people, as NSArray. Then, type .sort, we get a lot more things in auto-completion. Let's see. There we go. So, we have this whole bunch of sorting methods. This is a lot more powerful than what we have in Swift, and we're going to look at this one today. This is a method that's sorted array using an array of sort descriptors. Oh, sorry about the error.

So, this array of sort descriptors is something really powerful. With a sort descriptor we can, for example, sort by last name, and maybe in an ascending, or descending order. By using an array, we can have multiple ways to sort. We can first sort by last name, and then if there is a tie, then we can sort by first name. See how we can use that. We're going to create a sortDescriptor ... Wait, I'll make some white space below, so that you can see it a little bit better. Okay.

So, we're going to create an NSSortDescriptor, and then we'll use this initializer. This takes a key, the key that we want to sort on, so let's say last name. Then, if it's in ascending order or not, so let's sort them in ascending order, and then the selector. There's actually a nice syntax for working with selectors. Under the hood, they are still strings, but we can say selector, and then we can say something like NSString.localizedCaseInsensitiveCompare. Let's see. Yeah, this one. Now, we can use that sort descriptor. Over here, we can pass it in, and if we did everything right, it sorts.

The thing is, we will get a runtime error once the compiler is done. It's happy, but I think it will crash. Hopefully. The problem is that this last name key that we’re using, it becomes a little bit more apparent if we use the key path syntax. So we can say #keyPath, and then say, Person.lastName, close it. Now, the compiler will say, "Hey, person with last name is not an Objective-C property." We can actually use fix all in scope. Let's see if this goes right. Now, you can see it added this add Objective-C.

We're dealing with a struct. If you're used to working with structs, you know that this is basically nonsense, so we need to make Person a class in order to work with this, and let's make it an NSObject. This is one of my first problems with runtime programming, because it doesn't work with structs yet. Now, hopefully our array sorting works. Let's see. There we go. Make this a little bit bigger, and make this a little bit bigger. My mouse is very tiny over here. Okay, yeah, so now we're sorted by last name.

Let's rename this, actually. So, we call it lastnameSD. I like long names, it's just for the sake of the demo I'll make it short. We can copy/paste this, and make a first name sort descriptor, so let's do that. We need to change the name over here, and over here, we need to say person.firstName. Now, the cool thing is we can sort by last name, and then by first name. Let's add a tie over here, we'll add another person value. We'll actually add some connector. Now, we can sort arrays, and you can see that the first sorts by last name, and for the two Allen’s, there's a tie, and then he uses the first name sort descriptor.

In all fairness, this is a really cool idea. I think it's very powerful. It's very dynamic. We can create these sort descriptors on the fly, we can even create these arrays on the fly, based on like when a user clicks on table headers for example, in a Mac app. That's cool, but there's also some problems with it. If I would say person.yearOfBirth, then the code compiles, but it crashes. It actually doesn't crash yet, because the year of birth is the same for the two values, but if I would change, for example, this one to 33 or something, now we'll get a crash.

The reason that it crashes is because this key path, this person.lastName, which is a string value, doesn't match up with the selector, which is a NSString.localizedCaseInsensitiveCompare. If we try to use NSString.localizedCaseInsensitiveCompare into integer values, of course, it crashes. That's one of the problems.

Another problem is that we can also write completely nonsensical keypads. I'll just undo this, and if we would say, NSDictionary dot something, let's see what comes up. I don't know, this one. Of course, this will crash as well. Now, we have a slightly different problem, because we are passing an NSDictionary key path, but we're dealing with Person values. That's another problem.

I think I've shown now, hopefully, why this isn't as nice as it could be. Let's delete all of this, and re-implement this. First, we're going to sort People, just with the Swift sort. Now, we need to provide this function. We need a p1, and a p2, and then we need to, so for example, say p1.lastName.localizedCaseInsensitiveCompare, and then say p2.last name. This function needs to return a bool. The result of .localizedCaseInsensitiveCompare is a comparison result, so we need to check whether it's order is descending. Let's see. Okay, so this way, we can sort a People array. I have a slow computer, so it takes a while. Yeah, there we go. We can sort, and this is all fine. It's not really easy to sort by multiple properties yet, but will look at this in a little bit.

So, let's take this code, and put it out into a variable, so that we can also make a first name sort descriptor. We'll call this last name sort descriptor, and then over here, we can say last name sort descriptor is going to be a function from Person, and Person to boolean, and then we'll paste it back in. So here, we have our last name, and we can use fix all in scope, my favorite Xcode feature, to add the label.

Now, if we want to sort by first name, we can just copy/paste this, and change it around. Over here, we have a first name sort descriptor, and then over here, we also say first name. When I first wrote this, I thought it was done, but I have a bug in my code, and now we're comparing first name with last name in the first name sort descriptor, so clearly, this is a worse approach than what we had before, with the NSSortDescriptor, like you cannot make that mistake with an NSSortDescriptor.

In order to fix this, we're going to pull this shared code out. Well actually, just fix it before I forget. We're going to pull this shared code out, and write sort of a function that generates the sorting function. This function is going to be called sortDescriptor, and we need some arguments, I don't know yet what exactly. It needs to return a function, so let's return a function from person, to person, to bool. The arguments, well, we can start simple, and only work with string properties at first. We need some kind of property, and this needs to be a function again from Person to string. This sort of gets the string out of the person.

Now, how do we implement this? Well, we can start by looking at the return type. We need to return a function for person, to person, to bool, so we just write return, and then take a p1, and a p2, and then we can write the rest of our function. In here, we only need to return this bool, so we can just take this line, and move it in. Now, we're still sorting by last name, so we need to use this property. We can do that by saying property p1, and property p2. Property, there we go. Now, we can rewrite our sort descriptors, they will become very short.

There is one more problem over here, and I used fix all in scope to fix it, and so we have to mark this property as escaping. Escaping is a very important keyword. I don't have time to explain it today. It's important, but we'll let the compiler edit for us today. Let's rewrite this. Now, we can say sort descriptor property, and then say $0.lastName, so this becomes nice and short. Over here, we can do the same thing. We can say sort descriptor, and say $0.firstName.

This is kind of nice. This is nice and short, and we have this, and we have this abstraction, and all is good. But, we don't want to hardcode this localizedCaseInsensitiveCompare, so we want to sort of pull that out as well, so let's have a look at the type of it. I'll create a variable, localizedCaseInsensitiveCompare, and put it in, and then we can option click on it. I'll zoom in, it's probably very tiny. It's a function from string, to string, to ComparisonResult. I'll just copy/paste it. Here we go. We're going to put that in as a parameter as well. Over here, we'll have another parameter, compare, and put it in.

Now, over here, we can say localizedCaseInsensitiveCompare, and over here as well, and let Xcode add the labels for us. I have to wait a little bit, and then we can add them. There we go. Now, we've sort of abstracted that part away as well, and I think it's now a lot more flexible. That's pretty cool, but we're still dealing with Persons, and still the dealing with strings. Oh, we actually have to use this compare function, I forgot. We can remove this part, and add the compare over here.

It's sort of a bit of a weird function, because it takes a string, and then it takes another string, and then it returns a ComparisonResult, so this code function syntax; don’t worry about it too much, it will go anyway. So, we add another @escaping keyword, and now, I think, if we did everything right, our sort descriptors should still work. Let's wait a little bit until Xcode is done. Or maybe I made a mistake. We can scroll up over here a little bit. Maybe. Yeah, I think it's okay. This is my technique for getting Xcode to work, like add new lines and remove them.

Yeah, there we go. It works. Now, if we look at our sort descriptor function, it's not very specific to Person, and not very specific to string, except in a type. First, we can make it generic over the kind of property. So, if we add a generic parameter property, and then say, well, we want a function from Person to property. Then, we also want a way to compare two properties, and over here as well. Then, we return a function from Person, to Person, to bool. That's kind of cool. We made it way more generic, without really having to change the code. It's just a pattern that I see over and over again when I'm writing generic code. Often, you write a function, and it turns out that it's very generic, so that's nice.

So now, this Person, to Person, to bool function that we repeat everywhere, we can pull that out as well. We'll call this a sort descriptor, and we can pull it out by using a typealias. SortDescriptor, and just paste it in. We can actually change this to a sort descriptor as well, and over here as well, so that's kind of nice. It's a little bit more declarative, and we like declarative code, right?

Now, it's still very specific to Person, so let's try to pull that out as well. Now, with Swift 3, we have generic type aliases, so we can say sort descriptor for A is a function that takes two As and returns a bool if they're ascending. Now, we can change this to a sort descriptor of Person, and then let's just be complete, and do it over here as well. You don't need to type it always, but ... Here we go, now it's a sort descriptor of person.

Still, it's not very Person specific code, so let's pull that out as well. We can say sort descriptor for generic A, and a property, well, we need, instead of a function from person to property, we need A to property. Then, we have a sort descriptor of A. That's pretty nice, it's even more generic, and again, we didn't have to change the implementation. There is also a slight problem. It's this global function sort descriptor, this free function, and we like initializers, and we like to write things with the dot syntax.

Let's say we want to have a reversed sort descriptor. We could add a function reverse, but it would be nicer to say something like sort descriptor dot reversed. With this type alias for a function, it's not going to work, so we're going to put it into a struct. While we're at it, we're also going to change this comparison function. Let me put it back. We're just going to wrap it inside a struct, and instead of returning a boolean, we're going to return a ComparisonResult, and that's a much better abstraction, I think, or a much better type for the thing we're trying to do. We'll need to do a little bit of work there, but I think this is much better. In Swift, I think there are now, some proposals that are not active, but that might be, hopefully, become active again, that let us work with the sorting methods, with ComparisonResults instead of booleans.

So, our sort descriptor function, how do we sort of make this initializer? Well, it's actually not that hard. We can say extension SortDescriptor, and this way, we keep our memberwise initializer. We'll put this in, and indent it a little bit, it's always nice, and we write init. We can now remove this A generic parameter, because the sort descriptor already has a generic parameter, so if we remove it, now the property is from A to property, that looks fine. The comparison method looks fine. Now, we don't need a return type, so let's see, we can delete this part.

Now, in order to do this, we don't need to return, but we need to assign to self. So, we can say self =, and then we use the memberwise initializer. If we write it as a trailing closure, actually, this is all we have to do. I think the compiler should be happy about this part. Now, we get everything we want, and we can write this with a capital S. There we go, now we have a sort descriptor. Let's see, the compiler is unhappy about something.

Right, so it's this .orderedAscending part. Because we changed the way our sort descriptors work, and now they need to return a ComparisonResult, we need to actually remove this, and compare will return a ComparisonResult for us. Now, everything works, except for here at the bottom. Our last name sort descriptor is now a real type, it's a sortDescriptor, is not a function anymore, and even if we would write compare, so if we were to do something like compare, then this returns a ComparisonResult, and we need to return a boolean. What we can do is say, isAscending. I don't know if this is the best name, but that might be one way to implement a method called isAscending, which sort of generates that Person, to Person, to bool function for us.

So, we can just say add a function isAscending, and it needs to take two As, and return a boolean. Well, we have to compare a function there, so we can just return compare, put in the l and the r, and then check if it's ascending. Now, hopefully, we can sort by last name again. This is still working. Let's check the output to make sure we didn't break anything in the meantime. Looks good to me. Make it a little bigger. Yeah, it's still okay.

So, we've improved the situation a little bit, but there was this really nice API that we had for sorting with an array of sort descriptors. Now, the question is, how do we implement that, and where do we implement it? We could extend array, or maybe, as we've seen in Nate's talk, we could extend sequence, or maybe collections, or mutable collection, and add like more overloads of sort and sorted. But then, we would have to write the same method twice, once for them immutable version, and once for the mutable version, and it sort of explodes.

Instead of solving it that way, we can actually take a bunch of sort descriptors, and turn them into a single sort descriptor. If you remember, a sort descriptor is really just a function, so we can just take a bunch of functions, turn it into a single function. Let's see how we can do that. We can write a function combine, and it takes an array of sort descriptors, so a sort descriptor of A, and it needs to return a single sort descriptor of A, and then, we need this A as a generic parameter, of course.

How do we go about this? Well, the first thing is easy, we need to return a sort descriptor. We'll return a sort descriptor, and we'll again use this memberwise initializer, and then we get like a left and a right value. Now, we have two values, and we have this array of sort descriptors. The way the method on NSArray worked is it first tries the first sort descriptor. If the result is either smaller or larger, then we return. Only if they're equal, if they're the same, then we start looking at the next sort descriptor.

We can iterate over the sort descriptors. We can say, for sortDescriptor in sortDescriptors, and then we can check for the results. We can say, let result = sortDescriptor.compare, put in the left and the right value, and then if they are the same, we need to continue with the next sort descriptor. We can say, if result == .orderedSame { continue }. Now, after this if condition, we know that they are not the same, so we can just return it, and the compiler will not complain, because we don't have a default return value. We still need to return something. Let's think about what happened. We iterated over all the sortDescriptors, we called compare, and apparently they all returned orderedSame, so then the two values must be the same, so we can return orderedSame.

Now, we can combine our sortDescriptors. We can call combine, put in the array, and first sort by last name, and then by first name. Let's see if the result is okay. So, we're sorting by last name, and by first name, and if there's a tie, we're sorting by first name. I think it looks correct, and I think this is pretty cool. We didn't have to touch anything on sequence, or collection, or whatever, we can just build this on our own, without really having to do much work. It's only 50 lines to replicate almost all the functionality that the NSSortDescriptor has. And, it's completely type safe. We cannot accidentally provide an integer, and try to sort strings. For example, if we say year of birth over here, then of course, the compiler won't let us. That's really nice.

Just to quickly recap how we managed to do this, before, we had a KeyPath and a selector, and those were sort of problematic. The key path for accessing the property, we just wrote a simple getter function, $0.lastName, and for the selector, we just passed in a function. Then, by massaging these functions around, and combining them, and using generics, we were able to have a completely type safe API for sort descriptors. That's all I have to say, and I'm really happy to answer any questions. Thank you.


Q&A


Q1: 

Chris: Why did I choose to use orderedSame in the other ones, instead of booleans? 

When you work with booleans, you need to call the function twice, once for ascending, and if that returns false, you need to call it again for descending, and I didn't really like that. I think a ComparisonResult is much clearer than a true or a false. You don't really know by just looking at the type, if you see a boolean, you don't really know what is this boolean going to mean? What does true mean? What does false mean? In a ComparisonResult, you see orderedAscending, orderedDescending, and orderedSame, and it's very clear when you read that code that you're dealing with sorting.

In a previous version of this talk, actually, and in our book, Advanced Swift, I use booleans. Then, actually, somebody commented on that, and they said, "Well, it's not very nice to use booleans," and I agree. The nice thing about booleans is that they work better with Swift today, but I think, when somebody told me, I realized I was using the wrong type for that, yeah. Very good question, thanks. 


Q2: 

Chris: The question is: Why does it have to be a class? 

I knew I was going to forget something, so actually, let's remove it, and let's go down ... Yeah, it still works. So, we don't need classes anymore.


Q3:

Chris: The question is: Are there any other style APIs where this would be a great application for? 

I think there are many, many APIs, like almost every API. For example, if we ... Well, I think one really cool thing that we can always do is add more generics. Of course, at some point, this is going to complicate things, but if you look, for example, at user notifications that we saw earlier, or NSNotificationCenter, you can make that typed as well, to make sure that you always having your user info off your NSnotification, that you always know what the type of the object is going to be that's going to be in there.

So, I think generics can really help in almost every API, and make almost every API clearer. We can see this, actually, in the Swift, in sort of the wrappers around Cocoa, that they get added, like more and more generics get added, and the APIs just become that much better. The other thing is using functions a lot. I think this can really help improve a lot of APIs.

The other question is, like for example, one problem with my current approach is that you cannot just use String.localizedCaseInsensitiveCompare directly with a sort descriptor, or you cannot really sort with a sort descriptor directly. You always need to call this isAscending, so it's also, the approach that I use, we're wrapping it in a struct. Sometimes, it might be better to work with plain functions, instead of structs, and that also sort of depends on your use case, but I think, yeah, having functions and generics, and using them more would benefit almost every API.

I think, you know, it's very important to not be dogmatic about it. Classes are great, object oriented programming is great. It works really well. There are many great apps that are written using it, and I use it every time, and all the time. I think this sort of approach is really good to have in your toolbox, because sometimes, you have a problem, and then, like if you know object oriented programming, you can solve it, that's fine.

If you know functional programming, you might also have a different solution. Depending on the problem, one of the two might be better, and knowing more and more techniques to solve problems, I think, is always good, and will always help you find a better solution. The other point is, don't overuse it. Try to learn it, and try to look into all the possibilities you have, and then choose whatever works best. Thank you.