Brian Hicks0:08
Cool, so this is Robot Buttons from Mars. In the next half hour, I'm hopefully going to be teaching you some things about how to design good APIs. More formally, here's my goal: by the end of this talk, I want you to be able to explain what a good API is to a coworker or a friend. That way, not only do you have the information in this talk and you're able to apply it to your codebase, but you can apply it to somebody else if you find these things valuable.
A theme for the day today is we've been referring to Elm SBA example, and well, here we are once again. I've written a post on this blogging platform that Elm SBA example implements called Robot Buttons from Mars, and I want to sort of go over the interface here and try to show some things about the story of how we make code for things like buttons. You'll notice something about the buttons here: we have three different kinds of buttons and five different instances of these kinds of buttons. We have the Edit Article button with a grey pencil on top, we have a red Delete Article button with a trash can, and they have a green rounded white text Post Comment button. I know from having used this app before that the Post Comment button is actually the primary button style for MSBA example in the real world application implementations in general.
Let's try and take a first try at this button, at this Post Comment button. Intuitively, we might make an API that looks like this, where we have a button that takes a message (something that it does), a string (something that it says that it does, so our users have a nice affordance to know what they're about to do), and returning some HTML message so we can use it in the rest of our app. An instance of this button might look like button post comments, those comments on good message maybe notwithstanding. This looks okay, and now we have something concrete: our Post Comment button. This probably will work okay at first.
Let's try and implement another button. Let's try for the Edit Article button. We go from button of message of string to HTML message to maybe outline button, and we have the same signature there. We've probably taken our body of this function (which I'm not showing any of these for simplicity's sake, we're just talking about the API here) and copied it down to outline button, where we modified the things that are relevant. There we go, now we have an outline button. Sweet. Okay, now what about Delete Article? Well, we've got a little problem here, which is that the Delete Article is red. It's a dangerous button. We've got this sort of UI paradigm that says red things are things you don't want to do, or things that we want to discourage you from doing as they decrease engagement or whatever, right? So come on, how are we gonna make an API that supports this Delete Article button? Well, we might try to say something that says here's a danger button, and it takes the same signatures before, and we probably did the same copy and paste dance before to implement it. But that's not actually a great name, because danger button implies that it's a button, but our button is that solid green thing and it's more of an outline button. So really, we have like danger outlined button. I'm gonna stop with this train of thought because it shows enough of where we get into trouble with this sort of intuitive line of thinking. This is good for a little bit, but you know, we have a bunch of options already here. We have three different roles: we have a primary, secondary, and danger roles. We have styles: we have a button and an outline style. Some of our buttons have icons, some of them don't. And we all can already see from the screenshots that there are different sizes. So we're getting ourselves into trouble here by implementing all of these buttons individually.
I want to address something: you might look at this API, this three-button API, and say, 'Well, this is a straw man. Obviously nobody would ever do this.' Well, I've done it. In fact, I just refactored some code out of Notre Dame's codebase that did this. We had a tooltip API where somebody said, 'Well, the tooltips will always be on top,' and then some person months later said, 'Oh, well I found a use case where the tooltip needs to be on the left.' So tooltip left, great. And then some other person months later probably came along and said, 'Well, I need a tooltip that's on the right, oh and it's orange.' And so we had tooltip left, tooltip right, tooltip orange. This happens all the time when taken just instantly like this. We think that these decisions are really unreasonable, but taken over the course of months or years, somebody says, 'I implemented the feature and incidentally I added danger outline button.' We go, 'Oh yes, that looks fun. Passes code review, this is great.' Then we get ourselves into real trouble here.
So while I've titled this talk Robot Buttons from Mars, the real thing that we need to be cautious of, that we need to think about how we deal with in our code, is the robot options from Mars. It's just that buttons are emblematic of this problem because web apps are secretly mostly forms. I think we all know this in our hearts and just kind of try to deny it. 'No, this isn't a form. It's beautiful, it's an experience.' We're form programmers. I'm really good at making people other people. So robot options for us. How do we get out of this situation? Well, the first thing to do is realize a better way to get ourselves instead of getting out of this situation, to not get into this situation. We have this intuition, especially those of us who learn JavaScript before we learn Elm, to try and start things correctly because refactoring is extremely hard. But in Elm, everything is really easy to refactor, as we've seen time and time again today. So we have DRY: Don't Repeat Yourself. I want to introduce a new principle, or one that I probably stole from Twitter and then forgot: WET: Write Everything Twice. And more importantly, as Noah told us (and yes, I added this slide after your talk), have a conversation with your team. When you find that things are like this, have a talk about it. Figure out what you want to do as a group, because you're the people who are most affected by the code. Well, I mean other than your users, but they just engage with it.
All that said, say you're just copying stuff around, writing everything twice, having these conversations routinely. When is it actually time to find a better API? We see this advice 'don't abstract too early' all the time, but it's very rarely followed up with 'and here is when to abstract.' I have two principles that I think are really nice. The first is when features and fixes fall through the cracks. If you've got a bunch of instances of buttons and you're trying to change some style in them and you miss one, then you're going to create an inconsistent experience with people who are using your site. And then there's this unfortunate thing that happens: if your site isn't consistent, most people, especially non-technical people, will use the site and think, 'There's something wrong with me because I can't use this site. I can't make this computer do what I want.' But really, it's just because we made the website bad. So this is a problem. Let's not gaslight our users. And the second thing is for us: when our API is frustrating to use, or when it's slow, then it's time to find a better API. And of course, having these conversations with your team throughout is really helpful. Communication is really great.
So when we're talking about a better API, what does a good API actually look like? I want you to think about the best API you've ever used. Maybe it's Elm slash HTML because it mirrors the HTML document structure and it's recursive, and it does so precisely and so nicely. Maybe it's like Stripe's HTTP API. A lot of people like it because it has great documentation and nice client libraries. So what's a really good API that you have used? I think a good API is a stop sign. This may seem a little bit counterintuitive, but think about it: you're driving through a city that you've never been in before, and you see this octagonal red sign that says 'Stop' on it. What do you do? You stop. You don't run out in the middle of that intersection and get hit by a car. That would be foolish. And we know that these are consistent. We have a good user experience using them. City governments and city planners can take these signs that look like this and put them at dangerous intersections, and again, fewer crashes and fewer people injured. So the thing that we take away from the stop sign being a good API is that good APIs produce consistency. Consistency is important, as I said with the users of our sites that we don't gaslight with our bad interfaces, but also because we want to have consistency as developers. We want to do the right thing more easily. But stop signs are not always sufficient. If you go to different countries, sometimes stop signs are slightly different, but we need to be able to have kind of the same stop signs because we need to have variation within that constraint.
A little bit about the history of the stop sign. I'm just gonna... this metaphor I'm camping on. So good APIs allow variation with constraint. Stop signs started off in Michigan, and the US Transport Department said, 'Hey, that's a good idea. We're gonna use those.' Then the Canadian government said, 'Hey, that's pretty good there. We're gonna take that too.' And then the UN in 1968 met in Vienna and said, 'Hey, these stop signs are a pretty great idea. What if we standardize them so that people stop going into intersections unexpectedly and crashing across Europe?' 'Oh yeah, that's probably a good idea.' They standardized all sorts of road signs, and they came up with these standards. So we have a stop sign. We have an acceptable variant of a stop sign with a yellow background. We have this other common stop sign in use before 1968, which is a circle with an inverted triangle in it. And they all say 'Stop' no matter what it is. But sometimes 'Stop' itself is not a helpful thing to say. Sometimes you need something local because people don't speak English. So we've got a bunch of different things. In Argentina, Bolivia, they say 'Pare', which means 'halt' in Spanish and Portuguese. Well, we can just look at stop signs. This is fine. My favorite ones are like Israel up there, which is not even saying 'Stop' in like script. It's just a hand sign saying 'Don't'.
Let's take these principles that we've learned from stop signs about consistency and variation within constraint and apply them to our APIs. So our first try here was button, outline button, and danger outline button. Is this good according to our criteria? Well, not really, no. It's not consistent. It doesn't allow for variation without copying and making more inconsistency. The one thing it does have is constraint, but unfortunately we're over-constrained here. We can't actually express ourselves very well. So let's try for something better. I freeform added our original button, and let's try and extend it a little bit, taking as acknowledgment that we have options and then we want to model them. Maybe we use custom types and then add those custom types and construct them and put them into the arguments of button. So now we explicitly have our role as primary, secondary, danger, and our format is button or outline. This already sort of intuitively looks better. And we look at our call sites. This actually really does look better. So now our Post Comment button is a primary button, and our Delete Article button is a danger outline. So this is obviously better. Is this button API good? I think it is. It's consistent and it allows us to express variation within constraint. So that's great. But we only added two options, and we already know that there are more like width and icons that we haven't represented yet.
So what happens when we add more buttons here? I've added a bunch more, and these are all things that I've used in real production codebases and that we need to implement to have a successful implementation of the real world API. So we have enabled, which is enabled or disabled (for example with form validation). We have loading, which is idle or loading (again as user affordance so that when you click the button, you don't just go click click click click, you know that you actually did something). We have our icons, which we've already been over. And width, which is exactly this: wide fit to the content or fit to the container. And our size, which is small, medium, or large. So now how do our call sites look? They no longer look nice, right? We can't actually see what we're trying to override. And all this is to say, 'Give me the thing that's the default, that is the garden-variety standard button inside this application.' Is this too much work? I think it's probably not. And when we look at the Delete Article button, oh, you blinked and you missed a slight change. Well, what changed? Right, like four things changed. I'll just go back and forth, but you have to do this in order to understand what changed in the Delete Article. So is this a good API? No. We lost consistency. We can no longer express ourselves consistently in order to make a consistent API for the people who are using our site. So what does this tell us? I think it tells us that nice options, these beautiful bespoke custom type model options, are necessary but not sufficient for a nice API.
Let's take this as a whole now that we've got all our options in it and try to make it a little nicer. Let's try our arguments. I see this quite often: just put them in a record. Okay, that's better. The type error messages certainly just got a whole lot better, because instead of saying 'You passed me six arguments and I was expecting eight, and also all of them are wrong,' now it says 'I expected a record and you passed me a record without icon. Please add one of those and I will be happy.' The messages are much, much better here. This is a better API. It's more humane to the programmer. And our call sites are also similarly nice. We still have to specify everything every time though, so it's not consistent. So despite the fact that we made an objective improvement to the quality of life for the programmer, our API is still not consistent when viewed through the lens of the people who are actually using the site. So I think we need to add a new item to our rubric here, which is that good APIs set good defaults. Defaults. Good APIs set good defaults.
So what do we do here? Well, we know that we have a message and a label always, and the rest of it is sort of optional. We can make overrides. So let's move all the things that are optional to a separate record and then require that as a separate argument. As a bonus here, we get currying to work again, which is always nice. So how do the call sites here look? Oh, sorry, I should explain: we have all of these defaults here. This is the default button, the garden-variety thing that I mentioned earlier. These are just the normal defaults for the Post Comment button. And we can see that here, the Post Comment button just has the default options. We say it's normal. It is the default button, and we can express that with our API. And our danger button now says we have a dangerous looking button. It's a danger outline trashcan button. It will be afraid of the trash. So is this good? Well, yeah. It's consistent. It enables us to express variation with constraint, and it sets good defaults. But there are some caveats here. We have some problems with this API. First of all, you have to put default options in the global namespace in order to override it the way that I've done in the Delete call example. And if you have multiple APIs for multiple differently viewed doodads using this pattern, you have to really bend over backwards in order to use it correctly. You have to alias stuff inside your own namespace, include your own namespace in order to consume an API, which seems less good at best, right? And worse, if you just don't know where default options come from and you did like exposing dot dot or its default option. Second, it's not intuitive that default options even exist. So if you just naively say 'button blah, you know, delete article quote delete article,' the compiler will say 'I need a record that looks like this.' And you say, 'Ah, thank you compiler. I will construct one.' And then you just don't use default options. It's not intuitive that you can do the right thing. And part of consistency for us is doing the right thing.
Let's take our inspiration from a different source and try again. We can look to Elm slash HTML to get some more inspiration. And before we start there, after credit my colleagues do so for this idea of saying of what we're about to explain. You'll see we both realize that it's a bad idea. Spoiler alert. But thanks to them. So we have a list of attribute of message, a list of HTML message in the regular HTML API. So why don't we just say take a list of options, a message, and a string, and are normally there in our new API? Implementing this is a little bit of a runaround, but it's not terrible. So the first thing we need to do is create an option type, and we expose the type but not the constructors. And then we do expose custom constructors down below. We do this because it gives us the option to, at the constructor level, provide useful variations for the programmers. So here we have 'role' gives a role, takes a role, gives an option. And 'enabled' which takes an enabled (which we remember was enabled or disabled) and gives back an option. And instead, we could say 'enabled' takes a bool and gives an option. So if, for example, your form validation framework or whatever you're using for form validation can give you a bool very easily, you can now just pass that in and you have a nice affordance for yourself and an easier way to make correct code. Then after you have these options, you need to make something called 'customize', and you might call customize something different internally, but the important part is that it overrides the default options that we've already seen one at a time. So where we were overriding a bunch of defaults before, now we're overriding one default and then the next one and the next one, and we're doing this inside a list dot fold left inside of button. So once we have the options, we can do whatever we want with options. This is great, right? So our call sites are the cleanest yet. So our default button is just an empty list. So when the compiler says 'You haven't given me a list,' you say 'empty list' and you're correct. Intuitively, this is much better. And our danger button says 'I clearly have a role of danger and I kind of trashcan on a formatted outline.' Okay, this is better. I quite like this. It's intuitive, it's nice to use, it's clean, it resembles the other APIs that we use idiomatically in Elm. What's not to like here? Well, there's one thing. What if you specify role primary and role secondary at the same time? Which of these options will take precedence? If you think about the implementation I showed you, like I said, it is a fair bit of runaround. You remember that we folded left on our options, so primary would be set and then secondary would be set, and secondary would be the final option. I don't like this because it means you have to know deep API implementation details in order to avoid an edge case, in order to avoid doing something that you would sort of naturally do on your own maybe. And the compiler won't help you. It won't say 'issue roles here' because that might be valid code. On the other hand, you can use the same work to provide an API level override. So we had defaults at the option level. Now we can have defaults at the API level, and this is actually much nicer. So our delete button can set a danger role, a trash can icon, and an outline format, and those defaults will be respected wherever you use delete button by default. And then you can also provide options that override those defaults, so that if you need it, for example a green delete button to delete something you wanted to delete (for example, haters). So is this API good? Well, it's pretty good actually, all things considered. And without that edge case, it's consistent, it allows us to express variation with constraint, and it provides us a useful set of defaults. But there's this elephant in the room: is it okay to rely on incidental implementation details like this? This has been the bane of many a team. So much ink has been spilled over whether this is okay or not that I don't even want to get into it. You can decide whether this is okay for you as a team. For me personally, I'd say it's probably not. I don't like that I have to know implementation details to use this API correctly. So maybe we need a new rubric item in our good rubric, which is that it prevents misuse. And the HTML-like API, according to me at least, does not prevent misuse.
So here's our final criteria: good APIs prevent themselves from being misused. To go back to our stop sign analogy, stop signs don't prevent themselves from being misused, and with actually some really serious consequences. There was a 1991 study done that found that if you put a stop sign somewhere and people don't think it's necessary, they'll run it and cause an average of I think it was two hundred ninety-one thousand US dollars a year in terms of wear and tear on cars, damages, enforcement costs, pollution costs, noise abatement. This is just a staggering amount of money for something that's like a two hundred dollar stop sign. It's kind of ridiculous. But in our code, maybe we can... you can't always prevent things from being misused, but we can try. And I think we should try to prevent things from being misused in our code. Maybe we'll use types. Everybody loves types. So let's go for one final try on this API. Maybe we'll make something that's actually really nice. Call this the 'with star' pattern. You start with type 'button of message'. So instead of working with HTML now, we're working with buttons. And we call them buttons because they're buttons. Buttons. And then we have a constructor for button that takes a message and a string and returns the button message. And this looks really similar to our simplistic, over-constrained button thing from earlier, but now we're constructing a type of our own that we control. And finally, we tell this 'take this one and call to HTML on it' to turn it into HTML. So by itself, this is not useful. But because this is a type and we control it in our namespace, we can unwrap it and rewrap it to do useful things. So with 'role', it just takes the case or the override from customize and from the defaults record and does that thing in line. And it's like three lines for each one of these options that you need to customize, and then whatever customization that you're already doing in the view code. So how do our call sites look here? We have a button to post a comment, and we just send it directly to to HTML. This is nice. And our Delete Article button is much clearer as well. Same as before, it's a danger trashcan button. Oh, danger trashcan. Okay, so this is better. I think you never see an Elm pipeline where stuff goes backwards, and if it did, it would be extremely counterintuitive. So don't do it, right? This is not how idiomatic Elm code works. So this API, I think, is pretty great. And you still get the benefit that you can provide API level defaults. So our delete button just needs to take a button and call those things on it, returns a button, you can override whatever you need afterwards. Cool. So is this API good finally? I think we have one that's good according to all the criteria that I could think of of what makes a good API. It's consistent. It enables variation with constraint. It sets good defaults. And finally, it also prevents misuse. There's no vague ambiguous corners to this that you can get caught on.
So recap time. Good APIs do four things. First, they produce consistency. This is both for us as programmers to enable us to write better code by default, and for our users in order to avoid making them think there's something wrong with them for our crappy websites. Second, good APIs enable variation within constraint. So we want to express, for example, a delete button is different than a save button is different than an edit button is different than a cancel button. There are so many types of buttons, and we need to vary them all, but we need all of them to look like buttons, so we need constraint as well. Good APIs set good defaults, so that we know that we're doing the right thing by default. And finally, good APIs prevent themselves from being misused. So let's run through our evolution of an API real quick, the steps that some of us have certainly taken (I certainly have taken all of these steps in different APIs) and see how things went. We started off with this: a button, outline button, a danger outline button. This is not consistent, over-constrained, doesn't set defaults, doesn't prevent misuse because you can just copy whatever wherever, right? So not great. So we moved all of those options into arguments, and this was better, but then more options came and it was suddenly inconsistent. Then for programmer economics, we moved everything into a record, and this was kind of nice, but it broke currying, which I mean I love curry, it's great. So after we got that out, we kind of fixed currying a little bit by setting some defaults in a record that only overrides the things that we need to override. And once we realized that that was actually insufficient, or maybe that it was just difficult because of how we have to structure code in the call sites, we maybe created an HTML-like API, and this was nice except for that corner case that I mentioned. And so we discovered that maybe it's nice if we can prevent misuse in our API. And finally, we ended up in the 'with star' pattern. Some of you might also know this as the Builder pattern, where we have a button and we have customizations, send them through a pipeline, and end up into HTML. Which, depending on your team and circumstances, you may also call 'view'. So that's the end. My end recommendation for view code is to use the 'with star' pattern. I've been using it for all of the view code that I've been writing for probably the past six months. We built the entire Home Comp 2019 website with this pattern, and it's been really great. But I recognize that you and your team might have the same conversation and come to a different understanding of what you think is a good API. And in that case, you should ignore me, or even better, you should figure out your opinion about what is a good API, and we should all have a conversation about this so that we can drive our industry forwards to making better things for people to use. Computers are advisable for the line, right? Let's make good bicycles at least. Anyway, all that to say, thank you so much.