Writing Swiftly and Clearly

Why code clarity matters for good Swift developers.

danielsteinberg

Transcript:

Daniel Steinberg: So what I wanna do instead is share a recipe with you. This is a recipe that I got from a book I picked up last week in Scotland. It's a wonderful math book by Eugenia Chang called "Cakes, Custard + Category Theory". And of course what caught my eye was the category theory.

00:22: (Daniel motions to slideshow, which currently says: “You aren’t going to talk about Monads and Functors are you?”) No, I'm not. So here's the recipe. It's a recipe for lasagne and the recipe was this (Showing 4 ingredients on slides). Four items in lasagna, and the method is really easy. You lay down some bolognese, you lay down some noodles, you lay down the béchamel, and you repeat a couple times. You top it with parmesan and you bake it. And what she points out in this recipe is actually there's a lot of complexity hidden here and it depends on your context.

If you're a chef or an experienced home cook you know a lot about bolognese sauce and you know what that involves, and the ingredients there, and the method there. If you're slothful you'll just buy a jar and open it, and you know that too. Béchamel, you won't buy in a jar 'cause that's nasty so you need to know that for béchamel you use these ingredients (Showing 4 ingredients on slides). You make a roux from the butter and flour. And as it heats up and cooks off some of that flour you add the milk and when it gets to the right consistency. So the point is, context matters. A simple recipe changes depending on what you know about the context.

So the point is, context matters. A simple recipe changes depending on what you know about the context.

01:28: So I wanna start with an example from Objective-C from a class we teach. And my Objective-C example is a simple timer. So here's my timer, I hit the run button, it starts to go. And so, I'm not gonna show you all the code for this, I leave that as an exercise. But let's look at the model, and inside the model the way we get the time that we're displaying on the screen is with this elapsedTime method. And the elapsedTime method, first we create a date instance that is this moment in time, and then we use it to compute the NSTimeInterval and then that's what we return. And it turns out if I don't use this method but instead I use another method, I can get rid of all this code and just replace it with this simple call, timeIntervalSinceNow. And that almost works. What you see on the screen is this. And so, if we're building an android app this is okay.

But we have taste and so we don't wanna display a negative number here. So, we go back to our code and we add the negative sign here, and now we've caused a problem for all future developers 'cause every time they come to this line of code they have to figure out why there's a negative sign there. The problem is, timeIntervalSinceNow goes in the direction opposite of what we'd expect so we need that negative there. What we really want is a method timeIntervalUntilNow but it doesn't exist. So we're gonna add it in a category, and we of course know because this is Objective-C we've gotta namespace it a little bit to protect it. So, we have our category on NSDate where we declare this method. And then, over in the .m file we implement the method, and we implement the method timeIntervalUntilNow by returning the negative of timeIntervalSinceNow. And you go, "Daniel, it's the same negative sign that you hated." But I say context.

In this context it's fine because in this context it calls out to you and says, "The negative of timeIntervalSinceNow is timeIntervalUntilNow," that makes sense to me in this context where it didn't in the actual code. So this I consider to be clean, I consider to be clear, and if we look back at our lasagne then we see here's where we used the method and so we don't have to display what the complexity is because we're just measuring the timeIntervalUntilNow. And then, in our béchamel sauce that's where we hide our complexity. So as a next example... Oh (showing slides).

04:22: So in our next example, let's look at this model in Swift. And the model in Swift I really like because if you've done any amount of cooking, the way that cookbooks often will break these things out is they'll have the ingredients and then they'll break out the béchamel on the same page, and that's really what we do in Swift. In Swift we have our struct for our timer, and inside our struct we call our elapsedTime, and we use our timeIntervalUntilNow. And then, at the top is our béchamel sauce, our specifics, it's an extension in Swift. It goes in the same file. We don't have to namespace it, and we return the opposite of the timeIntervalUntilNow, and we feel pretty good 'cause in Swift it's even cleaner and more compact, no semi-colons.

...and we feel pretty good 'cause in Swift it's even cleaner and more compact, no semi-colons.

Now, you might say I cheated because this was a simple example, we just ported a simple Objective-C example and you wanna know real Swift, the Swift you can do to impress your friends, the Swift you can do to kick sand in Objective-C people's faces; map, filter, reduce. So here's a silly example. I wanna look at app sales, and so we have an app that we're selling in the store and we're tracking sales. I'm gonna generate app sales, instead of just using a simple array I wanna do it this way so that it comes back to bite us later. So, let's import GamePlayKit and once we've got that we're gonna create a SequenceType and so you're gonna tell me how many days you want information for, and I'm gonna generate the number of days as a Gaussian distribution, that has zero as its lowest, 10 as its highest. In other words, I average five sales a day. A lot of days I sell four or six, less so as I go out to the edges, until things fall away. And so, I have my Gaussian distribution, I am gonna generate my data using a generator. And so, as long as I haven't exhausted the number of days I'm supposed to generate for, I'll give you the next Intfrom the distribution. And then once I've exhausted it, I return nil.

06:33: So here's how I create seven days worth of data (showing code on slide). I find that stunning. You don't have to know about all that other gunk. You just look and say, "Oh, my app sales for the last seven days, there it is. And AppSales is a SequenceType. And one of the wonderful things about sequence types in Swift 2 is that's where all the fun got placed. Protocol extensions put all the fun in it, and in addition, for-in works beautifully with sequence types. Because for-in just goes next, next, next, until it hits the nil and then it's done. So fast enumeration works like this (pointing to slide). You don't even know there's a sequence type behind it. And we print out our daily sales, we get our numbers mainly clustered around five. That was impressive.

...one of the wonderful things about sequence types in Swift 2 is that's where all the fun got placed.

So the other thing about sequence types is that map, filter, reduce- that whole family of things is defined there. So we can take our sales over the last week and calculate our revenues for each day of the last week. And so we can take our sales and apply a map to it, and for each element in that array, we can calculate our daily sales like this. We can take our daily sales, how many I've sold that day times $1.99 'cause, I'm a worthwhile app, times the 70% that Apple lets me keep. And when I do that I find out that my revenue looks like this (pointing to slide). Just stunning.

Now, 1.99 is out of context, and 70 is out of context, and even more there's nothing that really says to me 1.99 represents a dollar amount, and 70 represents some percent amount. So maybe what I should do is provide that context and use some explaining variables and pull that 1.99 out, as the unitPrice, and pull the 70 out, as the sellersPercentage. And now this is beginning to read nicely. It's beginning to read like a recipe. This makes sense to me, except for the double thing, but... You know.

08:40: But if I can pull out unitPrice and sellersPercentage, maybe I should also pull out this calculation to make it even more readable. And so I will. I'll create, just like we do explaining variables, I'll create an explaining function and move my calculation there. And now last week's revenues looks pretty nice. Last week's revenues looks like something that, I can get rid of all the details and read this, and understand basically what I did. Now, I'm not a real Swift programmer, unless I use dollar zero instead.

...just like we do explaining variables, I'll create an explaining function...

Actually, I prefer this version, which I find very readable and very communicative. The only thing that bothers me is map. But we won't see what's wrong with map until I introduce a second map. So let's go on and adjust our distribution. I'm gonna widen it so that I pick numbers between negative five and 15. And so they're still clustered around five, but they decay more slowly. Of course, negative sales, we're gonna say that on those days I didn't sell any, so I'm gonna count negatives as zero. And I could use filter. And we could filter like this (pointing to slide). And we could pipe from filter to map this way, and filter out all the ones that are greater than zero.

Filter might change the size of the array. And that may or may not be a problem if I wanna keep a week's worth and assume that I have seven items, then instead of filter, I'm gonna use map. Now I confess that I really used map to show you this slide. Dollar zero, and zero, and colons... Because this helps newbies come to our language and feel warm.

10:32: So we can again take the code out of there, and pull it into a separate function, and now this is beginning to read a little bit better to me. I take last week's sales and I map it using this negativeNumbersToZero function. And then I map it using the revenues per copies sold. And so the good news is, if you sort of squint the right way, you can see the process that you are applying and our code is getting more readable. Don't take pictures, it's gonna get better.

And so the good news is, if you sort of squint the right way, you can see the process that you are applying and our code is getting more readable.

I'm still bothered by map and map (pointing at slides). And I'd like to get rid of map and map but I don't know how to get the thing that feeds into map yet. And so, we're gonna come back to lasagna in a minute, but I first wanna return to the béchamel. I know, someone tweeted, "What? Are you gonna write spaghetti code?"

11:21: So I look at revenuesForCopiesSold, and it returns a double, and you saw all those horrible looking doubles. So, first let's document that that double really isn't just any double, it represents dollars. And so let's return with the typealias and clean this up a little bit. But if I am returning dollars, I should probably round it to the nearest penny, and so let's introduce this function that rounds it to the nearest penny (pointing to slide). And now combine these two with the function revenue and dollars per copies sold. And what it returns is what you get when you take the number of copies, you calculate the raw revenues and then you round it out to the nearest penny. And that's kinda nice because I had a clicker, and it was color coded for you. But it's backwards, right? When you come to this code, you're gonna see it like this, and that doesn't make any sense at all. He said, "Well, we should do method chaining, then we can dot things together." But if you do method chaining you would have to add these functions with an extension to the int or to the double class and I don't wanna use method chaining in extensions.

(Daniel is showing multiple slides)

12:43: Let's introduce a Custom Operator. Don't push it.

So I'm gonna introduce an infix operator. And an infix operator operates on the thing that came before the thing that came after it. And it's also gotta be associativity one way or the other, 'cause I gotta tell you, if I have a bunch of these in a row, do we do them from the left or we do them from the right. So I'm doing these from the left, I'm just gonna flow from left to right. And so now let's go ahead and tell you how this is implemented. So I've got this operator, and the thing on the left is the input to my function, that is the thing on the right. So I'm gonna pass you an input and a function, and if you look, this is a good use of generics, not a gratuitous use of generics 'cause we're specifying that the thing on the left is something of type T, and the function takes something of type T. And the function returns something of type U, which is good because my operator returns something of type U. Now, if you've ever taken a functional programming class, it's at this point that someone says something obnoxious like, "Clearly, this can only do one thing."

...clearly is kind of in the eyes of the beholder.

But, clearly is kind of in the eyes of the beholder. And so we've gotta get used to this, but the way this thing will work is if we apply that function to the input that all of our types match up and we get something of type U out. And what's nice is if we go to use this in our revenuesInDollarsForCopiesSold, instead of that chain that worked backwards, now I start with my number of copies, I pipe it into my revenuesForCopiesSold, and I pipe that result into my toTheNearestPenny, and that's a custom operator use for good, not evil. But, we're not talking about béchamel sauce, let's go back to our lasagna. I had these maps, and in this context map feels to me like the minus sign.

14:42: People that love to talk about map say, "Look, you don't have the for loop’s in your face." And I wanna take it a step further and say, "Well, why do I have the map in my face?" A map is an implementation detail. It doesn't really help me understand what I'm doing at a high level. So, I wanna get rid of this map just like I got rid of the minus, I wanna move it aside. And so, I wanna take my maps and I wanna create methods where these are inside of them. Now look at the top map, the top map takes the last week's sales and the result is what it gets when it applies negative numbers to zero to it. The second map takes that result as the input to the second function. So I'm just going to create these two functions that abstract it, that pull those out.

And the nice thing is if we look, our types match. The output of the first function is the input of the second function. So we can pipe one to the other. Except not all the types match, because last week's sales is not an array of ints, it's a sequence type. Which I did so that we'd have a third map just as a gratuitous thing. And we'll map our sequence type to an array because that's what comes out of map. And now we chain them together and now my lasagne's looking pretty good. I take last week's sales, and from that I calculate and create an array of daily sales. And once I have that I look at all the negative values, and I take all those negative values and turn them into zeros. And now I take all of those ints that say how many did I sell each day, and I replace those with whatever the revenue is from sales. And I'm feeling pretty good because this reads like a bulleted list, it looks just like my lasagna. It looks just like this. Right?

I'm feeling pretty good because this (code) reads like a bulleted list; it looks just like my lasagna.

But that's not the way you write Swift code, is it? You write Swift code like this. And you nest things in there, and you write horrible things like this (pointing to slide).

And you check it in with like a gratuitous git command like, deal with it.

16:57: So what we'd rather do is zoom in. In our lasagna example we said, "Well, here's our ingredients, let's zoom in on the béchamel, there's more detail." What I love about this is I can do this here, here's my process, here's my top level, here's what I understand. If I want to understand this step more, I zoom in on it. And if I want to understand this step more, I zoom in on it. And not only that... Not only is it clean and clear, but each of those little steps is testable. So earlier today a speaker quoted another speaker's book and said that, "The purpose of functional programming... One of the hallmarks of functional programming, is you can break in to smaller, and smaller steps." But a lot of us don't do that. And I'm suggesting if we do that, then we have this clean, clear, testable code, and the context is clear. Now, there's one issue remaining. And the one issue remaining is, maybe this isn't efficient. At which point you throw up your hands and say, "So what? You want everything?"

Not only is it clean and clear, but each of those little steps is testable.

So Rich Hickey, last year in his Clojure/Conj talk, talked about this trolley example. And this trolley example was; you're loading an airplane with this pallet full of stuff that comes in. And these pallets come in and so the first thing you do is you rip off the plastic and you, from each pallet take out all the luggage, and that's just flatMap. And then, once you have the luggage, you go through each piece of luggage individually and you look for food because we're not bringing food on this plane, and that's just filter. And then as the last step, you go through each package and any of them that are too heavy you put a label on the side so these people don't strain themselves.

18:42: And so, we do these steps the same way we do arrays, but the problem is each array is created as an intermediate step. And so, Rich suggested something called transducers, other languages do it with compiler optimizations, and the idea is map, filter, flatMap, all these things can be reimplemented in terms of reduce. And if we get something in the shape of reduce maybe we can chain them together. And so, we open up that first pallet, grab the first bag, see if it's food or not, slap a label on it, and pass it on. We don't do those intermediate trolleys, we only do a pass once.

Now, we don't have a solution for that in Swift yet. And a lot of people have looked at this and tried to do third party solutions, you might have seen some of these checked in. And they're clever and they're good but it's something that should happen at either the compiler or the library level so we have to wait to see what happens. In the meantime, what do we do? We write our clear, our clean, our testable small bits of code and we continue writing Swiftly Clearly. Thank you.

Q&A:

Q1: Great talk. I was just wondering, on the int, if lazy could be used for that optimization? Or is it something else?

Daniel: Lazy is something that we have and that we do use. What I didn't say in this talk is... And what Rich has talked about is this same map. What made him look at transducers was he noticed he was reimplementing map every time he did a new something. So he was looking at stuff coming in on a stream and redoing it. He was looking at things that were generated on the fly and redoing it. And so, for those you couldn't do a lazy, you don't know the size of things. So he was saying, "Since I'm reimplementing map all the time, let me do this." Essentially, it's giving us a lazy but our current lazy keyword doesn't cover it.

Q2: Really great talk, thanks. Just on the surface it does seem like the approach that you're suggesting seems a little bit in contention to the dot-syntax protocol oriented programming that's being pushed by Apple. Could you comment on that?

Daniel: So the issue... I love what they've done. I love what they've done with protocol extensions. The issue, and believe me, if you've ever heard me talk about custom operators and generics and how much I want you to avoid them, but how do you get into this... So map used to, in the Swift standard libraries, take two arguments. It took the collection and it took the thing that you were doing to it. They moved it to sequence type and so now you can do it on any sequence type this way. If you move to this other mapping, how do you add it so that you get this as your input to that? And so, it's what we've thought about with bind and other ways of piping through solutions. So I don't think that it's counter to their thing but I think it might push them to then implement this so that when we do something like this their compiler can say, "Oh, I recognize this. I can optimize it."