Problem-solving techniques to avoid yelling at your computer

Problem-solving techniques to avoid yelling at your computer

A guide about how to solve problems and not die trying

I have been facing some tough code monsters at work lately. I guess I'm out of shape and my sword isn't sharp enough, because I find myself struggling greatly.

This is the reason I decided to upgrade some of my attributes (especially intelligence and wisdom) in order to help me cope with this struggle. Luckily, I found a great loot: Think Like a Programmer, by Spraul, V. Anton.

Think Like a Programmer

This book explains how we should approach problems in order to solve them in a methodical manner. I have just started reading it, but I have already learned the basics of problem-solving techniques, and I wanted to share them with you.

General Problem-Solving Techniques

1. Always Have a Plan

When working on a solution to a problem, starting by typing code without a clear direction in mind is not a good idea. Sometimes I found myself typing without putting too much thought into it, just for the sake of building something fast and, if I’m "lucky", stumbling with the solution to the problem. This is an awful approach.

According to the book, we should always have a plan. In the beginning, the plan usually will consist in how we are going to find the solution to our problem.

Our plan will, most likely, require alterations along the way, and this is OK. Without a plan, we are directionless, we miss on evaluating possible scenarios and different alternatives to find a solution.

“I have always found that plans are useless, but planning is indispensable.” - General Dwight D. Eisenhower

I prefer to think about a plan as a map that allows seeing all the possible roads to a destination (or even draw new ones as they manifest along the journey).

The book emphasises that plans allow us to set intermediate goals, and achieve them. This is of extreme importance because by achieving these intermediate goals we feel we are making progress towards the solution. As we are usually going to struggle until the solution is found, feeling that we are making progress will help to cope with despair.

2. Restate the Problem

A problem that at first seems incommensurable, could be expressed in other terms, or in a different manner. At first sight, we might think the problem is one way, but upon restating it we can discover that it's something different.

Change of perspective

Restating the problem in new, simpler or more familiar terms, could help us to redefine it and to come up with different solutions (hopefully, easier to implement than what we first thought would be).

Restating the problem could be thought of as one of the planning steps in solving a problem, and if we do restate the problem, we will be making progress towards the solution.

3. Divide the Problem

You don’t eat an elephant in one bite but in multiple small bites. Actually, I have never eaten an elephant, and I wouldn't either, as they are one of my favourite animals! But the metaphor is still applicable.

If the problem is too complex, divide it into smaller chunks. If we need to ask for user input, process the data, and store it in a database, start by building a simple input field that prints on the console, and build on top of that.

If we need to build a complex loop, we can start with one line, or a couple of lines, and manually perform the tasks that the loop should do until we have a clear idea of what is needed.

This is the technique I use the most when coding and is the one that helps me to keep my sanity when facing complex problems.

4. Start with What You Know

After dividing the problem into pieces, we can start by solving what we already know how to solve. Later we can focus on the tricky parts of it.

Does our problem require finding unique values? If we know how to use a Set in JavaScript, that could help us to solve it.

By starting with what you know, you build confidence and momentum toward the goal. - Spraul, V. Anton

5. Reduce the Problem

If the problem is too complex or if it can’t be subdivided into smaller pieces, we can reduce its complexity by applying (or removing) constraints to it.

Should a user be able to drag and drop tasks in our to-do app? Let’s write step by step all that is needed in order to implement this functionality and eliminate (for now) what’s too complex. Later we could review what we have left out and ask for help in order to solve those specific aspects of the problem (and not the whole problem).

The author of the book uses an excellent example here:

One never wants to be reduced to saying, “Here’s my program, and it doesn’t work. Why not?” Using the problem-reduction technique, one can pinpoint the help needed, saying something like, “Here’s some code I wrote. As you can see, I know how to find the distance between two three-dimensional coordinates, and I know how to check whether one distance is less than another. But I can’t seem to find a general solution for finding the pair of coordinates with the minimum distance.” - Spraul, V. Anton

6. Look for Analogies

We are often solving the same problem, but in a different flavour, over and over again.

We should pay attention to similarities between the problem we have in front of us, and other problems we solved in the past.

If the problem is totally new, that’s great, because it’s a great opportunity to put it inside our “cookie jar of solved problems” for future use.

7. Experiment

If everything looks dark and we don’t know why the program is behaving as if it had life on its own, experimenting with it can help us.

I usually use console.log as a “probing” method to find out what’s happening. We can tweak something here and there and see how the output gets modified. Then make hypotheses and put them to the test.

It is important to make a distinction between experimenting and guessing. Experimenting is a controlled process: we gather the information that can help us solve the problem. Guessing is typing code mindlessly and hoping for the best.

8. Don’t Get Frustrated

The final technique is more of a recommendation than a technique in itself: don't get frustrated.

I must confess that I had felt frustrated many times, and I can assure you that it didn’t help me one bit to solve the problem.

How does the rage you feel inside of you help you solve a problem? Why are you angry? Or a better question even: why do you choose to be angry?

Here is where two maxims of the Stoic philosophy come for our help:

“We suffer more often in imagination than in reality.” - Seneca

And:

“The chief task in life is simply this: to identify and separate matters so that I can say clearly to myself which are externals not under my control, and which have to do with the choices I actually control. Where then do I look for good and evil? Not to uncontrollable externals, but within myself to the choices that are my own…” - Epictetus, Discourses, 2.5.4–5

We have to learn to manage how we respond to frustration. As developers, it’s very likely that we are going to face bugs in our code or complicated features to implement on a daily basis, and a good portion of them are going to be difficult to solve. Hence, it’s better to learn not to feel frustrated because of this, but rather think of this as a “feature” of our job.

I often remember this question:

What shit sandwich do you want to eat? Because eventually, we all get served one. - Mark Manson

And then I realise: "I have chosen this struggle, so I shouldn't get mad because of it."

Something that can help us to get out of a frustrating moment is to iterate on these steps again: build a new plan, restate the problem differently, create different problem divisions and alterations, etc.

Practice time

Now, let’s put all these concepts into practice. I have adapted one of the problems presented in the book from C++ to JavaScript for convenience purposes.

By the way, you can get the source code from my repository (I plan on keep adding new problems as I move forward in reading the book): think-like-a-programmer-book-problems.

Write a program that uses a single console.log statement to produce a pattern of hash symbols shaped like half of a perfect 5 x 5 square (or a right triangle):

#####
####
###
##
#

Spraul, V. Anton. Think Like a Programmer (p. 26). Adapted from C++ to JavaScript by Damian Demasi.

The plan

We are going to take the problem, divide it into smaller pieces, and start solving the pieces one by one. We are going to search for analogies along the way. In the end, we will put everything together.

Restating the problem

After analysing the problem a bit deeper, we can see that there is a pattern that needs to be printed and that this pattern gets altered on each line. I don’t know about you, but this is suggesting “iteration” to me, so we will use some sort of loop to solve it.

Our restatement could be something like: “write a loop that prints a line of hash symbols where each line gets one less hash symbol that the previous one, starting with 5 symbols on the first line.”

Divide the problem

We can divide this problem like this:

  1. Print a line of 5 hash symbols
  2. Print 5 lines of 5 hash symbols
  3. Find a way to decrease a number as a count grows
  4. Apply point 3 to point 2

Start with What You Know

Let’s start by the easy part: print 5 hashes:

    let halfSquare = '';
    for (let i = 1; i <= 5; i++) {
        halfSquare += '#';
    }
    console.log(halfSquare);

Output:

#####

Great! It’s not much, but it’s honest work 😅.

Reduce the Problem

Now, instead of printing five lines with a decreasing number of hashes, let's print 5 lines with the same number of hashes. By doing this we are simplifying the problem and reducing its complexity and constraints.

let halfSquare = '';
    for (let i = 1; i <= 5; i++) {
        for (let j = 1; j <= 5; j++) {
            halfSquare += '#';
        }
        halfSquare += '\n';
    }
    console.log(halfSquare);

Output:

#####
#####
#####
#####
#####

Look for Analogies

In the previous block of code, we are using something we already know (a loop) to repeat the line of hashes 5 times, resulting in a nested for loop. This is our analogy.

Experiment

Now we need to find a way to decrease a value as the loop increases. In the book, this is referenced as “counting down by counting up”.

Let’s experiment to find this behaviour starting with the easier part: counting up.

for (let i = 1; i <= 5; i++) {
    console.log(i);
}

Output:

1
2
3
4
5

After looking at the expected end result (the triangle of hashes) and comparing it with our experiment, we can think of these numbers as the number of hashes that need to be subtracted on each line plus 1:

LineResult
First line:5 hashes - 1 + 1 = 5 hashes
Second line:5 hashes - 2 + 1 = 4 hashes
Third line:5 hashes - 3 + 1 = 3 hashes
Fourth line:5 hashes - 4 + 1 = 2 hashes
Fifth line:5 hashes - 5 + 1 = 1 hash

If we use 6 hashes, we can get rid of the “+ 1” part:

LineResult
First line:6 hashes - 1 = 5 hashes
Second line:6 hashes - 2 = 4 hashes
Third line:6 hashes - 3 = 3 hashes
Fourth line:6 hashes - 4 = 2 hashes
Fifth line:6 hashes - 5 = 1 hash

So, as 6 looks like an important number for our purposes, and as the subtracted number looks like the for loop iterating as a new line gets printed, let’s see what happens when we subtract the current iteration number from 6:

for (let i = 1; i <= 5; i++) {
    console.log(6 - i);
}

Output:

5
4
3
2
1

This is starting to look good!

So, as the first line will need to have 5 hashes, the iteration should go from 1 to 6 - 1 = 5.

let hashLine = '';
for (let i = 1; i <= 6 - 1; i++) {
    hashLine += '#';
}
console.log(hashLine);

Output:

#####

Moving to the next line, to build it our loop will need to change the i <= 6 - 1 condition to i <= 6 - 2 condition. This pattern is pointing to a loop incrementing a value on each iteration, and we already have that loop: it’s the outer for loop.

When combining these findings with what we already have we can arrive to the conclusion of our problem:

let halfSquare = '';
for (let i = 1; i <= 5; i++) {
    for (let j = 1; j <= 6 - i; j++) {
        halfSquare += '#';
    }
    halfSquare += '\n';
}
console.log(halfSquare);

Output:

#####
####
###
##
#

And this is the fruit of the struggle, the thinking and the experimenting.

Closing thoughts

Applying all these techniques at once is not easy for me. I discovered that I have “my way” of solving problems, but it’s often not the most suitable way of doing so. As with most new habits, implementing a particular process comes with a certain resistance. This is why I’m taking the approach of implementing these problem-solving techniques one step at a time (and I’ve noticed that I’m going full-on "inception" here, using the division of a problem into smaller pieces as a technique to implement the problem-solving techniques 🤯).


Notion templates, trackers and roadmaps

🙋‍♂️ Psst! Before you go, I have something to share with you. If you are a subscriber of my newsletter, you already know this, but in case you are not, I would like to share with you these 3 free resources:

👍 My Notion template with over 440 pages of web development content, so you can use Notion to organise programming topics.

👍 My HTML Study Progress Tracker and Roadmap Notion Template, so you can keep track of your progress in learning HTML concepts (and review them).

👍 And my CSS Study Progress Tracker and Roadmap Notion Template, to to the same as before but with CSS.

They are totally free for download and use, but I'm always open to donations (I need to buy coffee to transform it into code 😅).


🗞️ NEWSLETTER - If you want to hear about my latest articles and interesting software development content, subscribe to my newsletter.

🐦 TWITTER - Follow me on Twitter.

Did you find this article valuable?

Support Damian Demasi by becoming a sponsor. Any amount is appreciated!