Lucas Tolchinsky

Less Code, More Confidence

Recorded in June 2018 during https://2018.rubyparis.org in Paris. More talks at https://goo.gl/8egyWi

Paris.rb Conf 2018

00:00:11.469 Hello everybody, thank you very much for having me. Welcome to this talk, 'Less Code, More Confidence'. My name is Lucas Tolchinsky and I'm here to tell you that I hate debugging.
00:00:16.240 Now, I know that there are better people than me out there who truly believe that behind every bug there's a lesson to be learned. That's a very romantic idea, but it's just not who I am. I'm pretty sure everybody in this room has a story similar to this one.
00:00:24.910 It’s maybe five, six, or seven in the afternoon. You're in the office, three to four hours into a debugging session that doesn't seem to have an end. You're just sitting there at your computer, making random changes to the code in the hopes that the bug will go away and you can finally go home. Does this sound familiar at all? I’ve been in this situation before.
00:01:02.219 Okay, cool. So what I actually hate about this is the feeling of being lost in the code. I know I'm missing something—it could be a devilish little detail or something super obvious that I'm just not seeing—but I know that I'm missing something. What I think is going on here is actually the issue of legacy code.
00:01:17.720 The way I like to think about legacy code is not as some old Fortran system running on super old servers, but rather as code that nobody on the team knows how it works anymore. It doesn't matter if you're using the latest version of Rails; if you're not able to go through the source code and truly understand how it does what it does, then you've immediately introduced legacy code into your project.
00:01:49.000 This is why it's so important to me to work on and write code that is easy to understand. I know that for every feature I’m running in production, there will come a time when how fast I can load that code into my head will make a huge difference when debugging.
00:02:07.940 This is why the 'less code' movement, or counterculture—however you want to call it—resonates a lot with me. It's this idea of favoring small libraries over very large frameworks. Simplicity, minimalism, less code—these are buzzwords that can easily be thrown around but are actually very hard to pin down when it comes to actual code.
00:02:29.150 So rather than discussing the philosophies around these ideas, today we're going to try to find where less code shows up in actual code. We're going to go over three principles, three ideas, and review some open-source libraries to find out how some of them favor these ideas while others go against them.
00:02:66.490 Now, before we actually start, I want to make a disclaimer: we are not the code we write, and it's very easy to forget this as it somehow becomes embedded into our developer DNA. So despite any criticisms that may arise here today, if you are writing open-source code, my thanks to you—because it's something that takes a lot of effort from many people.
00:03:02.859 If you're writing open-source code, kudos to you! So, let's get started.
00:03:30.389 Readability. Now, the way I see it, code has two purposes: it tells computers what to do, and it explains to humans how it's done. So let's take a look at our first example of the day: assert URLs.
00:03:45.519 Assert URLs is a small gem that I wrote. Imagine for a second that you have a /foo endpoint, and you want to test that when you are posting to it, the location header of the response contains '/4.1' as part of the path. Now, this is the test that you can probably write, and what assert URLs does is provide you with a number of helper functions to target individual parts of the URL without having to match the whole string. The nice thing about this is that this test is now immune to any changes in the host or the port or any other part of the URL that you don't really care about.
00:05:51.850 So let's dive into how assert URLs is actually implemented. When I first wrote assert URLs, I had each individual method written with its own body— a third host equal, a third path equal, etc. Then I realized that these methods looked pretty much alike. My refactoring reflex kicked in; I was taught that good developers don’t repeat themselves, so I wanted to keep everything as DRY as possible.
00:06:29.300 But after I was done writing this, I asked myself, 'Well, why write this one way? Who is this code optimized for? Is this code somehow more performant? Is Ruby going to load this faster into memory? The answer is no; this code is not optimized for anybody.
00:06:41.460 So I chose to optimize for readability. I returned to the original approach and expanded each of the method definitions with its own body. Now, of course, this does take a few more lines of code. So why is this better? By optimizing for readability, we quickly realize that the number of times we read code is actually a lot higher than the times it is written or changed.
00:07:13.940 But beyond this, optimizing for readability makes this code easier to load into your head, which in turn means less legacy code. Readability is a huge topic; we could discuss it for hours: how we write the variables or the long names, or not. But at the end of the day, it’s all about the form or the shape—it’s how we write what we write.
00:07:58.060 In this next section, we'll discuss explicit content. Explicit is better than implicit—this is one of the 20 principles in the Zen of Python. But beyond the generality of it, what it means for us as library writers is the balance between what we choose to show versus what we choose to hide.
00:08:08.040 The balance between these two is what we call abstraction. Let's take a look at an example: device. How many people here know about device? Okay, pretty much everybody! So this is a Ruby gem for those who don't know it. It's used for user authentication.
00:08:21.440 What you're seeing here is pretty much everything you need to get it up and running. You require device and tell your model, 'Hey, device, this model is database-authenticatable.' Then, in your application controller, you simply call before_action to authenticate the user. This is super practical and will get us running very fast.
00:08:55.510 But it’s what I like to call a dangerous abstraction. Imagine for a moment that you actually write this code, ship it to production, and then a couple of weeks go by, and your boss informs you that the CEO of the company can't log into the website. Now it's debugging time. You sit down and say, 'Okay, let's make sure all my parents are going to the right places.' You start by looking at the authenticate user method.
00:09:34.740 If you dive into Device's source code, you'll find this. There isn't much to go on, but you can see here that this method just says 'authenticate' and stops. At this point, you have two problems: one is that you have a bug in production, and two is that you need to find out what it is and if it's related to your problem.
00:10:02.150 Why does this make Device a dangerous abstraction? Because you're learning about how it works at the worst moment possible, which is after you have implemented it and it's already running in production. So, how does explicit look in practice? Let's take a look at an example.
00:10:30.630 It's a library called Shield. It's also a Ruby gem for user authentication, but it works a little bit differently. First, you need to define a method named fetch, which receives a username, a plain string that could be an email or whatever you're using. Instead of it, it is our job to actually reach into the database to retrieve the user.
00:10:54.220 After that, we can call user.authenticates, giving it the username and password. So let’s take a look at how 'authenticate' is actually written. In this code, we can see the line where 'user equals fetch'. The nice thing about this is that after those two or three weeks, when you're debugging, seeing this will help you make the mental connection back to when you were implementing it.
00:11:25.180 What’s very interesting here is that Shield is not hiding everything about authentication from us; it makes us a part of the solution. It makes the strategy explicit while also hiding complexity since we don't need to care about checking the password or anything like that.
00:11:51.430 Now, even though hiding complexity is the very reason we have abstractions in the first place, hiding the strategies behind our solutions leads to feelings of black magic. By making our strategies explicit and our solutions clear to the user, we will have less of that feeling and, therefore, less legacy code.
00:12:28.570 This next section is my favorite one, and there's no better way to introduce it than with a wonderful quote that says, 'Perfection is achieved not when there's nothing else to add, but when there's nothing else to remove.' Let's talk about avoiding redundancy.
00:12:37.420 What I mean by redundancy is basically giving your users the choice of doing one thing in more than one way. While this may sometimes be convenient or even fun, flexibility can be a double-edged sword. When you have multiple ways to do the same thing, it becomes harder for a group to write uniform code.
00:13:09.760 Homogenous-looking code is actually more predictable, and more predictable code is more readable. This quest for homogeneous code is not new; for example, Python's significant whitespace ensures everyone indents code the same way, or Go has a format tool that formats the source code for you.
00:13:41.060 Let’s see how redundancy looks in 'Fat', a gem I wrote before we had 'dig' in Ruby. This gem stands for 'Find, Add, and Fetch' and is meant to navigate a hash safely to avoid the undefined method brackets-for-nil error that we all hate.
00:14:12.780 You can call 'Fat' in three ways: the first argument is always the hash, followed by the keys that lead to the final value. You can either provide each key as an individual argument or concatenate keys with a dot or colon to signify they are symbols instead of strings. Let’s dive into how 'Fat' implements this logic.
00:14:57.140 The core idea is that if I receive only one argument, I will try to split it by a dot. If that split is empty, I'll try to split it by a colon and convert the keys to symbols. In roughly 15-17 lines of code, we’ve added complexity without actually solving the main problem of navigating a hash safely. My point here is: is this actually worth it?
00:15:35.760 If I need those features, I could just remove the concatenation feature and have everyone pass the keys as arguments. More importantly, how can we find redundancy like this in our daily code? One question I like to ask myself whenever I write a class or piece of code is, 'Are all my users using 100% of the code?'
00:16:16.440 This question isn’t new; we’ve already talked about code coverage in testing. It’s a similar idea when discussing the interface of a class, so interface coverage is important. Flexibility is easy to spot, but there are subtler ways flexibility can sneak up on you.
00:16:54.450 Let’s discuss 'Cuba' for a moment. How many people here have used it? Cuba is a wonderful HTTP routing library that lets you match part of an HTTP request with a block of code. For example, if you have a GET /home request, this piece of code means that line 5 will match the home part and execute the block.
00:17:19.540 Line 6 will match the GET, delegating execution to line 27, which will write 'I’m home' into the body. We can also achieve the same result by flipping the order of the nested blocks, first matching the verb, then the path, and finally executing the block.
00:17:49.790 Interestingly, Cuba's flexibility isn't baked into extra code, unlike Fat. There is no parsing to provide flexibility here. In practice, however, for every Cuba project I’ve worked on with teams, we always end up discussing how we write the routes: do we match all the GETs first, or do we structure our calls differently? This effort means we spend more time discussing code conventions.
00:18:23.050 The good news is that by keeping flexibility at bay in our tools, we can actually reduce the time we spend discussing these conventions. The final example I want to share is 'Zero', which is sort of an evolution of Cuba. This means they look similar, with the same idea: you have a GET /home route, and line 5 will match home, while line 6 matches GET.
00:18:53.370 You can then run into the body, but if you flip the order of the nested blocks, this request will result in a 404 error. The GET block won’t match the request because it checks if the path has been consumed, not if it still has something in it.
00:19:10.920 The nice thing about this is that all the 'Zero' applications look more alike than 'Cuba' apps, which in turn means we’ll have more predictable code. This, of course, leads to less legacy code.
00:19:32.920 So this is the end of the talk, and I just want to summarize what we've discussed here today. Less code, minimalism, simplicity—these concepts are very hard to pin down concerning actual code. This is my way of making sense of minimalism and this whole philosophy.
00:19:58.360 We discussed optimizing for readability, making your solutions explicit, and avoiding redundant interfaces.
00:20:06.620 As I said at the beginning of the talk, debugging can be hard, stressful, and very frustrating. I truly believe that these ideas will not only alleviate some of the challenges that come with working with each other’s code but will also take us one step closer to elevating coding into a form of art.
00:20:21.240 Thank you very much.