Becoming a Map-Maker
Daniel Steinberg at Swift Summit 2017
All of you know map? We'll see. I think about learning new things as sort of a Newton's method. You might remember Newton's method from school. You're trying to find out where a function crosses the X axis, and you can't quite find it, so you make a guess, and you start nearby it. Then you follow that up to the curve, and you learn in calculus that near a point on the curve, if it's a smooth curve, a tangent line approximates it, so you follow the line down and you go, "Oh, that's probably my second guess." And that’s hopefully starting to converge the the truth. So we're going to start with a version of map that we all know, and try to converge to the point that you can make your own maps, okay?
The one you all know probably from Swift is array, and so seasonally correct, I start with the name, and because it's almost Halloween, I map it to "Boo." So I have a function that takes a string, and it returns a string, and the string it returns is just "Boo," followed by the name, and then it's Halloween night, and three trick or treaters arrive, and because I live in a math neighborhood, they arrive inside of an array, and I want to send all of them the "Boo" message, and so we know that we do that sort of altogether. I want "Boo, Jill," "Boo, Ethan," "Boo, Maggie," but I have this function that takes a string and returns a string, so what I kind of want to do is apply f to each one inside and get the desired results.
But that's not what this function does, and so as we evolve to Swift's map, we start with a function that takes an array of strings and returns an array of strings, and it applies to this function by taking this output array and instantiating it, and then going through all of my names, Jill, Ethan, Maggie, and saying "Boo" to each one and appending them to the output, and hopefully, if I've done that properly, I'll get change Jill, Ethan, and Maggie gives me "Boo, Jill," "Boo, Ethan," "Boo, Maggie." That's awesome.
And so our idea of map in general is I've got this function that does one thing, and I want to lift it to this other world where I don't have strings, I have arrays of strings, and so when I do that, I've got this new function, and it's kind of like this morning, I'm sitting there at the hotel thinking, well, how do I get to the Palace of Fine Arts? But I have Apple Maps, and if I can solve this problem in Apple Maps, then I can solve it in the real world, and so I find myself in Apple Maps, and I say, Apple Maps, how do I make my way to the venue, and it says, well, you can go like this. I say, well, awesome. I'll go like that.
And so, I have this path in Apple Maps, and the idea is I'm going to lift that to a path in the real world that starts in the real world at my hotel, and then takes me to the conference here, and I just couldn't be happier, except that I don't have a car, and you know that this is directions for a car because it's respecting one-way streets in San Francisco and taking me the wrong way, and so I say, well, how about a path if I'm walking? And it said it would take an hour and that there were hills, and so I said, well, how about if I take the bus? So, back to our code, I don't want to hard code this function in here. I want to have the ability to pass in different paths, different ways to go from one string, maybe not even to another string, maybe I want to go from a string to an int, and just how many elements are there in the name, and so that changes things a little bit.
Now, change is going to have to return an int, and we do that for a little while, and we see that we're passing in this function, and we say, well, maybe it can be string to any output, and so I better have this generic output because otherwise no one will read my post on Stack Overflow, and so, I make very small modifications, and I'm feeling good about myself. I can change Jill, Ethan, and Maggie by using this, and I get 4, 5, and 6. Now, some of you know already that I have my own methodology called Hate Driven Development, and in Hate Driven Development, I look at code that I hate, and I change it, and so I hate that you pass in the array in the function.
I'd rather it looked something like this, that it just called change on the array, and then in order to do that, I have to place this inside of an extension for array, and little things change. I don't need input and output anymore. I don't need to hard code string anymore because array has this thing of type element, and so, the other thing I need to do, because I am the array, I'm iterating through myself, but the code is actually nicer, and I get the result I want, and I'm feeling really good about myself, except maybe dictionary wants a map, too, so I should put it in sequence, except that ruins my talk, so I'm putting it back in array, and I'm going to call it map.
So that's map for array, and so we can call it like this, and life is looking good, trailing closures are beginning to grow on us. We're not going to do the dollar zero here. So, we see map, not just from me getting from the hotel to here, but we see it in many places. My daughter used to be in marching band in high school, and in addition to marching band songs, they do a lot of covers. They'll take a popular rock song, and move it over like, you haven't lived till you've heard Crazy Train for a marching band, or Earth, Wind, and Fire or something, and so the idea is, if we know how to take the sheet music and adapt it from the original version to marching band, well, that's not so interesting. The interesting thing happens when we lift it into our world, and say, I'd like to hear not just the sheet music, I'd like to hear the performance. How does it sound different?
So, we're going to look at a different map, not just for array, and the other map that's in the Swift standard library is for optionals, and so maybe I have this function that takes the number of hours you worked and it pays you in euros, just in case, and so I return, after I accept hours, I return euros by multiplying the hours I worked times euros, and then I have the number of hours I worked this week, and I worked Monday, Tuesday, Thursday. I didn't feel like working Wednesday and Friday, and so my earnings is going to take a day and tell me how much money I made, but if I didn't work that day, then I don't get paid for that day.
I could return zero, but I'm going to return nil, and so I'm going to take weekdays and I'm going to return optional euros, and so in this case, I look up in the dictionary, and when you look up a value using the key, you get an optional, and if it's nil, I'm going to do one thing. If it's not nil, I'm going to do another thing. If it's nil, I'm going to return nil. You didn't make any money today, and if it's not nil, I'm going to look at how many hours you worked, and I'm going to multiply that by my 15 euros an hour, and so there's my earnings that takes weekdays and pays out in euros. That hours for the week of a day, that's an optional.
Oh, so I could look at a map for optional, and the map for optional is going to take whatever value it is, and it needs a function, not from an optional to an optional, but a function from whatever type the optional wraps to whatever type the output wraps, and so, if I give it nil, I get nil, and if I give it an actual value, well, I look inside that value, I bind to it is what we say in Swift, I map it over using my pay, and so I calculate the pay on those hours, and I return you an optional.
And that makes my earnings for really nice. There's no guard let. There's no if. I just take hours for the week, which is an optional, and I map it over using this optional version of map, and I pay, and life works nicely, and all of a sudden, I've moved closer to the truth of what map is, right? Because we were told map was all about for loops. Well, the important thing is now that we look at it for arrays, now that we look at it for optionals, we've got this pattern that we're beginning to see. We have this function that takes some input and gives us output, and we lift it to some other world using map of f, and map of f takes the world for this input and the world for this output and connects them, using this f somehow, and it's going to be different in every situation, but it looks something like this.
We start with an extension of world, and we figure out how we take this function that maps one type, whatever the world's type is, to its target, and maps the world of that world's type to the world of the target, and so map is beginning to have a set shape for us, and we're feeling pretty good, and we look back at array, and says, oh, well that matches the shape, and we look at optional and that matches the shape. So, now let's write our own map, and once you understand something, let's extend it and write our own map, so for result, the result type was something that people really liked before Swift had errors, and then they really liked it more after Swift had errors.
I know. So, I have an enum for result, and it either succeeds or it fails. If it fails then we wrap up the error in that case, using an associated value, and if it's successful, we wrap up that value of the successful value in the success case, and maybe I'm computing the hours for what day you worked, and the hours for what day you worked is going to take what day you worked, and look it up in the dictionary, and either there's a value or there's not a value, and so I take weekdays and I turn this result of hours. If it's successful that's great. It's just the unwrapped value in the dictionary, and if it's not successful, I return an error.
And so, what I want to do is take those hours I got back, and tell you what the pay is, but it could have been a success or an error, so I'm kind of mapping from a result of hours to result in euros, and all I have is this function that maps from hours to euros, and you say, I know what we need, right? We'll just use map. So this was my general shape. How do I adapt that to result? I say, well, the extension of result is going to have a map in it. The map is going to take some target value using a function that takes the value over, and I'm going to get this result, and it's just what you think it should be from optional. If I have an error, I'm just going to map my error across, and if I'm successful, I'm going to apply my function to that value and get the success for that function, and now life is so much easier. Monday's pay is just hours for Monday, map, using that pay function. Oh, and a dollar zero for bonus.
So, if I look at it so far, my hours for Monday is either a success or an error because it's a result type. For Monday, it's a success of three and a half. For Wednesday, it's a failure. I wasn't scheduled on Wednesday, and the nice thing is if I map this error across, it still tells me you weren't scheduled on Wednesday. It just maps it over to another error, but if I go back to Monday, where I actually did work, when I map that across, I apply "pay15for" to how many hours I have the actual value, as I look inside of it, and I get this is how much money I made wrapped up in a success.
And we're feeling pretty good. We've moved from the world of recipes, where you have a specific map for array, and a specific map for optionals, and we've moved to formulas, and we love formulas. Instead of getting tons of cookbooks with thousands of recipes for vinaigrettes, if we just learn that a vinaigrette is essentially this, it's endlessly adaptable, right? I can take that and say, well, here's a vinaigrette. Lemon juice is my acid. My emulsifier will be mustard. I'll add a clove of garlic just because you should, and I'll use olive oil as my oil. If you start with a formula, you can adapt it, so there's my formula. There's my idea for map. It's kind of a design pattern. Well, it can't be. It's not in the Gang of Four. It's a design pattern, and I apply my design pattern in whatever situation I am, and I got my map for result that way. Okay, well we were talking about vinaigrettes.
I have this, and I taste it. It's a little bland. I think it needs salt, so I add salt, and what I get back is the new state of my vinaigrette, something new to taste, and I also get the new sort of feeling about it. It's a little too lemony now. The salt has brought out that acid a little bit too much, so maybe I'll add some oregano, and have something sort of like a Greek style vinaigrette, and it's perfect. And this idea of mapping state along and having state return with something else is something we can actually use in our code, so I'm not going to walk through all of this, but I have a random number generator, and the random number generator is something I want to be able to use over and over again, and every time I use it, I get the same value. Not very exciting, so the value I get has got to be more complicated.
It's going to take a seed, and when I ask this random number generator to give me the next integer, not only does it give me my random integer, but it also gives me the next random number generator that I'm going to use to get to the next step. It's my vinaigrette as I season it, and the advantage of that is, I can always replay any stage, but I also get this testable, repeatable random number generator. Don't use this in shipping code.
We can abstract that to anything that has state and a run method, and the run method is going to take your state and give you the next state and whatever you're interested in, an A and the next state. And so, the A is what we call the int in our random number generator, and the S was the random number generator itself, and so, the typealias that we use can say, well, another way to think of this is it's just a random number that depends on say, int or double or anything else, and so here's how I create a random number generator for ints. I just pass in, so state has to get whatever the run method is, and I say, take the random number generator and give me back the next int, and so that's how I get int.
You say, but that's nice. I've got my random number generator that gives me the next int, but what if I want the next double, the next bool, the next anything? You say, well, it's a talk about maps. Map's going to be the answer, so state we can just create a map, and this shape is exactly the same. I have a function from A to B, a function from int to double, a function from int to bool, and what it's going to give me back is a new state that doesn't depend on whatever A is, it depends on whatever B is, standard map from type A to type B, and so I've got something that takes a rand int and gives me a rand double, a rand A and gives me a rand B.
Now, I caution you. You're going to see maps everywhere in your life. Don't be ridiculous, but at least here I can create a random number generator for doubles, and the random number generator for doubles is just going to take the one for int and map it using some function that maps an int to a double. The rest of the world takes care of itself. I have to say what happens with a single int, how we get a double, and so this is going to give me a double between zero and one, and I just use it how I did. If I want a bool, how do I get a bool? Well, maybe I mod it two, and if it's one mod two, it's true, and if it's zero mod two, it's false, and I've got a random number generator for bools, and they're everywhere.
And so I've got this design pattern, and so instead of just using the map for array and using the map for optionals, you're going to encounter times in your life where you're going to say, a map would really make things nice. I can write my own, and so here you are with this powerful tool, and now you can go anywhere in the world using a map. Thank you.