What I Learned About Life by Learning to Code
April 06, 2018
My journey in life is a good deal longer than my journey in code, and yet, I feel at times that my understanding of the latter exceeds my understanding of the former. Life is vast, wonderful, and complex beyond reasoning. Code is limited in scope and necessarily decomposable into discrete pieces. You can build a web app in an afternoon, but there is no framework for prototyping a relationship. Writing an O(log n) algorithm to search an array is straightforward, but no such algorithm exists for finding happiness. Life is not object-oriented, or functional, or declarative—it defies classification. No, life and code are not the same thing—but that doesn’t mean we can’t learn something about the one from the other.
Over the last several years I have devoted an immense amount of time and energy to the craft of code. I’ve learned a lot in that time but the things that stick with me the most have nothing to do with 1s and 0s. What I know now is that the lessons of code are all around us. I’d like to take some time to go through a few of them with you. If you’re a coder, perhaps they will be familiar to you. And if you’re not, maybe you’ll be inspired to learn to code too.
Structured Problem Solving
At its most basic, coding is about problem solving. Some problems are big, like designing a new piece of software from the bottom up, and others are small, like sorting a list of names so that they are in alphabetical order. The problems vary in scope, and complexity, and what does or does not make for a “solution,” but the same basic approach applies to all of them. It goes something like this:
- Develop a thorough understanding of the problem;
- Identify expected inputs and outputs to the problem;
- Draft test cases to validate your eventual solution;
- Model any necessary data;
- Develop an algorithm to address the problem;
- Implement your algorithm; and,
- Verify that your solution is correct and refactor as needed.
The intent of this kind of structured problem solving is to abstract away language-specific details so that you can focus on what matters. What are the actual parameters of the problem? What are the contributing factors? What constitutes a solution? You will note that this list does not include items like “install the Magic.js framework” or “download xyz problem solving library.”
Life’s problems vary in scope even more than coding problems, but elements of this approach are still instructive. To take a simple example, imagine the lamp in your living room won’t turn on. What do you do? Your first goal is to understand the problem (Is there a power outage? Did a fuse blow? Is the lightbulb out?) From there, you develop test cases to verify your eventual solution (Does the lamp turn on? Does it work in multiple outlets? Does it work with multiple light bulbs?) Next, you develop an algorithm (Replace the lightbulb… if that doesn’t work, then try another outlet… if that doesn’t work, then check the fuse box… etc.) Finally, you implement your approach and verify that your test conditions are met.
Our imaginary malfunctioning lamp could be called an engineering problem, and it therefore feels natural to apply an engineering approach. Human problems are harder. How do you identify the many and varied inputs and outputs to human emotion? How do you develop and test solutions for a strong relationship with your spouse? How do you know that a given job is right for you? It’s important that we not pretend that humans can be understood and treated like machines (they can’t); however, our problem solving approach is sufficiently abstract that we can still learn something from it.
Take the issue of finding the right job. As always, the first goal is to understand the problem (What constitutes “right” for you? Does salary matter? Is the commute important?). Next, we develop test cases to apply to candidate jobs (Does the salary exceed your expenses? Is the work engaging? Is the commute right?) From there, we decide on an approach (Can you “test” a job by doing it on contract for a few weeks? Can you talk to people who are already doing the job?) Developing an algorithm for finding the perfect job isn’t intuitive (or perhaps even possible), but systematic problem solving methodology at least allows you to understand the parameters of the problem. Only after you’re comfortable with your understanding do you even attempt to implement an approach.
Isolating Bugs
A lesson that is closely related to structured problem solving is how to isolate and address bugs. In coding, things go wrong a lot. It is an accepted part of a coder’s life that few things go right the first time around. You write a program to the best of your ability, using the aforementioned problem solving approach, and then you test it for successful outcomes. Most of the time your tests fail the first time around. Debugging is the norm rather than the exception.
The first step to good debugging is isolating the offending piece code. In order to address a bug you first have to find it. You might do this by setting up a breakpoint so that you can inspect values at a given point in program execution. Or, perhaps you would call the involved functions independently so that you could see if one was producing unexpected outputs. It’s an iterative and experimental process that demands a structured approach. Good coders spend a significant amount of time debugging because they know that small bugs created early on, but left unaddressed, inevitably produce larger, nastier, more pernicious bugs later. You squash bugs when they’re small so that you’re not left dealing with monstrous radioactive bugs in the future.
Debugging is an important part of coding. By learning good debugging practices we are acknowledging that things can and will go wrong. And that’s OK. In some ways, debugging is an exercise in optimism, because on top of the assumption that things can and will go wrong is a layer that assumes we can and will find and fix the problem. Why can we not apply the same attitude to other aspects of our lives?
In most cases, people have a tendency to assume that things will (or at least should) go right in their lives. It’s a hopeful attitude and one of the defining aspects of our species. This is not a bad thing. It lets us dare to do amazing things because we believe that they might actually be possible. But when things go wrong, as they inevitably do, we are often caught flat footed. Bad things don’t just happen, they happen to you. Misfortune feels very personal. But what if bad things happened in the world just because they happen, and that’s OK? Not only do they happen, but we have the capability to fix them. We view code in this manner—no one says that a bug happened to them. Bugs just happen.
A debugging attitude towards real life problems may not always be realistic (we are after all, people, and our problems feel personal because they are personal), but it can be a useful starting point. Ultimately, it’s an attitude that helps you develop a deeper understanding of your needs and address the things (the bugs) that are interfering. Imagine that you are in a long term relationship but find yourself feeling dissatisfied. What is the root cause of your unhappiness? What is the bug causing unexpected or unwanted outcomes? Perhaps it is some fundamental disagreement in values that you and your significant other can’t resolve (or have never talked about in the first place.) Perhaps you’re feeling unappreciated because you don’t spend enough time together. Maybe the problem isn’t your relationship at all, but unhappiness elsewhere in your life merely masquerading as a relationship issue. The potential causes are numerous, but if you don’t take the time to isolate the bugs, and try to understand them, the problem will only grow.
Abstraction
Operating at the appropriate level of abstraction means that you are focusing on the things that actually matter. When you dive too deep into the mechanics of how a piece of code works, especially early in your coding journey, you risk overwhelming yourself with unnecessary details. Conversely, when you operate at too high a level you risk failing to absorb fundamental knowledge.
The level of abstraction that you need depends on the situation. Sometimes you don’t need to know exactly how a given function works, you just need to know how to use it. Clarity is the goal of good code and clarity means different things to different users. The end user of a program doesn’t need to know how it is built. They just need to know how to achieve their particular goals by using the tools they are provided. A developer, on the other hand, should have a deep knowledge of how the program she is building operates. However, she probably doesn’t need a deep understanding of her chosen language’s particular implementation. Attempting to force people into the wrong level of abstraction does them no good and ultimately produces unwanted outcomes.
Modern software programs are incredibly complex and rely on a range of technologies to function. Attempting to understand all of them simultaneously would be a Sisyphean task. That’s why we break our programs down into easily digestible chunks, lest we find ourselves choking on too much complexity. In order to do this, we first have to look for the right level of abstraction so that we can focus on what matters and ignore the rest. Naturally, the “right level” shifts up and down over time, but compromise between our desire to know fundamentals and our need to discard mental clutter is a necessary part of software development.
As complex as they are though, modern software programs are nowhere near as complex as the social, philosophical, and physical systems that compose “real life.” Just as no single coder is likely to fully grasp the intricacies of every technology that goes into their programs, no single person can expect to understand everything that affects the direction of their lives. Abstraction is therefore a necessary exercise in understanding the things that matter to us.
What does this mean in practice? It means that if you have a particular goal in life, your first step is to identify the level of abstraction necessary to achieve that goal. If your goal is to learn to play the piano, you probably don’t need to know the exact frequency at which individual strings vibrate after being struck by the hammer. You do however need to know where middle C is, and how to read music, and what the elements of a given chord are. That is your level of abstraction. Trying to learn everything else is just clutter that will certainly prove distracting. When you’re an expert you can worry about learning how to tune a piano and figuring out exactly how they are constructed. (And doing so then will make you a better pianist, but trying to do so now won’t help you.)
Fundamentals
Speaking of middle C—a word on fundamentals. They matter. Deeply. Just because you’re abstracting away mental clutter doesn’t mean you should go for the easy fixes. If you want to know your craft well, you have to start with the fundamentals. This is true of coding and of just about every other endeavor you may embark upon.
I am by no means an “expert coder”. I haven’t been programming since coming out of the womb. There is still a lot I don’t know (and there always will be), but what I do have is an excellent understanding of the fundamentals. Part of that is about language syntax, but more important are the language-agnostic fundamentals like problem solving, data structures, scope, and design patterns. In the beginning, this approach to learning was often quite lonely and frustrating. I wanted to build things, not fiddle with seemingly arcane details of how a particular language operates. But the more I learned, the more I realized that those details are a necessary step towards mastery.
Now that I have grown in my journey as a coder, I spend more time playing with frameworks and fancy new libraries. But could I have learned Rails in depth if I didn’t know how Ruby’s class inheritance system worked? Could I really take advantage of React if I didn’t understand how this changes in JavaScript or what the DOM is and how it can be manipulated? The answer to both questions is “no”. It is certainly possible to acquire a working knowledge of any modern framework without having a deep knowledge base, but doing so will only get you so far. More importantly, an understanding of the fundamentals opens up a world of possibility. It means that you’re trading on core principles rather than shallow tricks. A good coder will have no problem picking up new languages and frameworks, but doing so requires them to apply fundamental knowledge to new situations (rather than constantly starting from zero.)
Fundamentals matter in coding and they matter in life as well. If you want to learn piano, you don’t start with Rachmaninoff. If you want to learn to bake, a four tier wedding cake probably shouldn’t be your first project. And if you want to have successful relationships, you have to learn basic empathy first.
Empathy
And that brings us to one last lesson: empathy. Learning about empathy from cold, heartless, mechanical code is a strange idea, but on deeper examination it is oddly compelling. As we have said, people are not machines (and shouldn’t be treated as such), but dealing with either requires a certain amount of empathy. As coders, we devote huge amounts of time to understanding why our programs do what they do. This is why we learn the fundamentals. This is why we study problem solving. This is why we isolate bugs. If you don’t understand how your program works, and why it works the way it does, you will have a hard time predicting what it will ultimately produce.
Computers are not human, and they do not have feelings, so “empathy” may seem like the wrong word; however, I would argue that our efforts to understand how programs operate is a kind of empathy. It is an acknowledgement that if we want to predict outcomes we first have to understand the rules of operation. Empathizing with a person is not so very different. At its core, empathy is an exercise in understanding. If you want to know the reason a person does what they do, you first have to understand their perspective and motivation. Empathy is a vital part of human relationships, just as understanding how a given language will execute a given piece of code is a vital part of programming.
Real life brings many opportunities for intense emotion. We are constantly bombarded by other people’s anger, and passion, and excitement. And we do plenty of bombarding ourselves too. When we encounter this in the real world our first reaction is all too often one of befuddlement, especially if we don’t agree with what another person is saying or doing. How could they possibly believe that?! we ask ourselves. But isn’t it likely that they are asking themselves the same thing, just in the other direction? This is ultimately a problem of empathy. We haven’t taken the time to understand the fundamental beliefs and experiences that are driving the other person, so we cannot possibly hope to understand their actions. And yet, as coders, when a program produces an unexpected result our initial reaction is to deepen our understanding so that we can produce better results in the future. The same should be true in real life.
The practice of code is not a perfect stand in for the practice of life. There are, however, some striking similarities. In programming, we practice sound engineering practices because we know that they produce superior outcomes. Perhaps we would do well to adopt similar practices in life. Problem solving, abstraction, a focus on fundamentals, and empathy are all ideas that can and should be applied broadly. The challenge is in knowing when and how to apply them, and to avoid the distraction and frustration that comes with temporary misfortune.
I’ve learned a lot about life from code, not because I believe that you can treat life like a machine, but because I understand that the critical thinking practices of good coding can help me address problems everywhere.
Note: This article was originally published on Medium.