10 Getting better at solving complex problems

This chapter covers

In the last few chapters, we mainly looked at what you should not do while coding and why. We investigated the effect of bad names in chapter 8 and the impact of code smells on your ability to understand code in chapter 9.

Earlier in the book, in chapter 6, we discussed different strategies to support your working memory when solving programming problems. This chapter again covers techniques to help you in problem solving, but focuses on strengthening your LTM.

We will first investigate what it means to solve a problem. After exploring problem solving in depth, we dive into how to get better at it. By the end of this chapter, you will know two techniques to improve your programming and problem-solving skills. The first technique that we will cover is automatization (i.e., being able to do small tasks without thinking about them). This is useful because the less time you spend on figuring out small things, the easier it is to solve hard problems. Then we will explore problem solving from others’ written code as a means to improve your own problem-solving skills.

10.1 What is problem solving?

The goal of this chapter is to teach you strategies that will help you improve your problem-solving skills by examining the LTM’s role in problem solving. But before we can tell you how to better solve problems, we will first dive into what it means to engage in problem solving.

10.1.1 Elements of problem solving

Problem solving has three important elements:

For example, consider playing tic-tac-toe. In that situation, the start state is an empty field, your desired state is three crosses in a row, and the rules are that you can place a cross in any empty field on the board. When you are adding a search box to an existing website, your start state is the existing code base and your desired state might be passing unit tests or a satisfied user. The rules of the problem in programming often come in the form of constraints, such as implementing the feature in JavaScript or not breaking additional tests while implementing this new feature.

10.1.2 State space

All steps that we could consider while solving a program are called the problem’s state space. When playing tic-tac-toe, all possible fields are the state space. For small problems like tic-tac-toe, the whole state space can be visualized. Figure 10.1 shows a small part of the state space of a tic-tac-toe game.

CH10_F01_Hermans2

Figure 10.1 A part of the state space of a tic-tac-toe game, where link arrows indicate moves for o’s and blue arrows are moves for x’s. The goal state for x is to get three x’s in a row.

For example, when adding a button to a website, all possible JavaScript programs are the state space. It is up to the problem solver to make the right moves or add the right lines of code to reach the start goal. In other words, problem solving means traversing the state space in the optimal way, reaching the goal state in as few steps as possible.

EXERCISE 10.1 Examine something you created with code within the last few days. What were the aspects of this problem?

10.2 What is the role of the LTM when you solve programming problems?

Now that we have defined what it means to solve a problem, we can dive into what happens in your brain when you do. Chapter 6 covered what happens in your working memory when you solve programming problems. If you experience too much cognitive load, your brain cannot properly process and programming gets harder. However, the LTM also plays a role in problem solving, as we will explore in this chapter.

10.2.1 Is problem solving a cognitive process on its own?

Some people think that problem solving is a generic skill and thus also a specific process in your brain. Hungarian mathematician George Pólya is a famous thinker on problem solving. In 1945, Pólya wrote a short and famous book called How to Solve It. His book proposes a “system of thinking” to solve any problem involving three steps:

  1. Understanding the problem

  2. Devising a plan

  3. Carrying out the plan

However, despite the popularity of generic approaches, research has consistently shown that problem solving is neither a generic skill nor a cognitive process. There are two reasons why generic problem-solving methods do not work so well, and both have to do with the role of the LTM.

You use LTM when solving problems

First, when you solve a problem, you take into account knowledge about the desired goal state and the rules by which we have to play. The problem itself will influence the solutions we can design. Let’s consider the influence of prior knowledge on solutions with a programming example. Suppose you have to implement code to detect whether a given input string s is a palindrome. We ask you to write code for this problem in Java, APL, and BASIC. Let’s investigate Polya’s steps and see how they would pan out when programming.

It is easier for your brain to solve familiar problems

There is a second reason generic problem-solving methods often fall short that also relates to the workings of the LTM. In chapter 3, we explained that memories in the LTM are stored as a network in relation to each other. Earlier in this chapter, we also covered the fact that while thinking about a problem, the brain retrieves information from the LTM, which could be relevant to the problem at hand.

Using a generic problem-solving technique like Polya’s “devise a plan” creates a cognitive problem. You might have a lot of useful strategies stored in your LTM, which the brain tries to retrieve when solving the problem. When we try to solve the problem in a generic way, however, the relevant strategies might not be found. As we outlined in chapter 3, the LTM needs clues to retrieve the right memories. The more specific the clue, the more likely we are to find the right memory. For example, when you have to perform tail division, thinking of a plan is unlikely to give your LTM enough clues to find the approach stored in memory. Thinking of things like tail division or subtracting multiples of the divisor are more likely to result in the right plan.

Like we covered in chapter 7, transfer of knowledge from one domain, like chess, to another domain, like mathematics, is unlikely to happen. Similarly, transfer from the very generic domain of problem solving back to other domains is not very likely.

10.2.2 How to teach your LTM to solve problems

We have seen that problem solving is not a cognitive process. This brings us to this question: How should we train for problem solving instead? To explore that in more depth, we need to dive even deeper into how the brain thinks. Earlier in the book we explained that thoughts are formed in the working memory. We also saw that your working memory does not form thoughts alone but operates in strong collaboration which both the LTM and the STM.

When you think about a certain problem, let’s say implementing a sorting button on a web application, your working memory will make the decisions on what you implement. However, before your working memory you can make such a decision, it needs to do two things. First is to get information from the STM about the context of the problem, such as the requirements for the button or the existing code you just read.

At the same time, the LTM is searched for relevant background knowledge. Relevant memories that you might have, such as how to implement sorting or information about the code base, are also sent to the working memory. To understand problem solving better, we need to explore this second process of searching the LTM.

10.2.3 Two types of memories that play a role in problem solving

Later in this chapter we will explore two techniques to strengthen your problem-solving skills. Before that, however, we need to explore different types of memories people have and what role they play when solving problems. Understanding the different forms of memories matters because different types are created in different ways,

The LTM can store different types of memory, which are outlined in figure 10.2. First there is procedural (sometimes called implicit) memory, which is the memory for motor skills or skills you are not consciously aware of. Implicit memories are, for example, knowing how to tie shoelaces or ride a bike.

The second type of memory that plays a role when solving a problem is declarative (sometimes called explicit) memory. There are facts you can remember, and you also know you know them, such as the fact that Barack Obama was the 44th president of the United States, or that the way to write a for-loop in Java is for (i = 0; i < n; i++).

As shown in figure 10.2, declarative memories can be subdivided into two categories: episodic memory and semantic memory. Episodic memories are what we often mean when we colloquially use the word “memory.” Those are memories of experiences, like going to summer camp when you were 14, meeting your spouse for the first time, or spending three hours chasing a bug only to find out there was an error in a unit test.

The other part of declarative memory is called semantic memory. Semantic memory is memory for meanings, concepts, or facts, such as that frog in French is “grenouille,” or 5 times 7 equals 35, or a class in Java is used to combine data and functionality.

Semantic memories are the memories we trained in chapter 3 with flashcards. Episodic memories are created without you spending additional effort, although, similar to semantic memories, retrieval strength will be higher for memories you have thought about often.

What types of memories play a role when you solve problems?

All these forms of memory play a role when programming, as outlined in figure 10.2 When considering the memories you use for programming, explicit memory might be the first that comes to mind. While programming, a programmer must remember how to construct a loop in Java. However, other forms of memory also play a role, as illustrated by figure 10.2

CH10_F02_Hermans2

Figure 10.2 There are different types of memories. Procedural (or implicit) memory showcases how to do something. Declarative (explicit) memory consists of memories we are explicitly aware of. Declarative memory is further divided into things you have experienced and that are stored in episodic memory and facts you know that are stored in semantic memory.

Episodic memory is used when you remember how you solved a problem in the past. When you have to solve a problem involving hierarchy, for example, you might remember that you used a tree in the past. Research shows that experts especially rely heavily on episodic memory when solving problems. In a sense, experts recreate, rather than solve, familiar problems. That means that instead of finding a new solution, they rely on solutions that have previously worked for similar problems. In chapter 10, we will dive deeper into how we can strengthen episodic memories to become better problem solvers.

In addition to both forms of explicit memory, programming activities also rely on implicit memory, as illustrated in figure 10.3. For example, many programmers can touch type, which is procedural memory. In addition to the alphabet, there are numerous keystrokes you can use without explicit attention, such as hitting ctrl-z when you make a mistake or automatically adding a closing bracket to an opening bracket. In problem-solving activities, implicit memory can also play a role, for example, when you automatically place a break point on a line where you suspect a bug might be present. Sometimes what we call intuition, in fact, happens when you solve a problem similar to one you have solved before; you just know what to do without really knowing how to do it.

CH10_F03_Hermans2

Figure 10.3 Different types of memories and how they play a role in programming

Unlearning

While we have seen that implicit memory can help you quickly execute known tasks, having implicit memories can also be harmful. In chapter 7, we discussed the idea of negative transfer: knowing something harms learning something else. Having a lot of implicit memory can harm your flexibility as well. For example, once you have learned how to touch type on a Qwerty keyboard, learning to use a Dvorak keyboard will be harder than if you had never learned Qwerty. This is partly due to the fact that you have a large set of implicit memories of how to do things.

You might have experienced the difficulty of unlearning implicit memory, as well, if you have ever learned a second programming language with a syntax quite different from the first language you learned. For example, when moving from C# or Java to Python, it is likely you will unconsciously type curly brackets around blocks or functions for a while. I personally moved from C# to Python years ago, and I still very often type foreach instead of for when iterating over a list, which is due to implicit memory I built when programming C# and that is still strongly present.

Exercise 10.2 The next time you write code, try to actively monitor the memories you use.

Use the following table to reflect on what type of programs or problems activate which type of memories. It can be interesting to do this exercise a few times for different programs. If you do the exercise a few times and follow your progress for a while, it is likely you will see that for more unfamiliar programming languages or projects, you rely more on semantic memory, whereas for more familiar situations, you use more procedural and episodic memories.

Program or problem

Procedural

Episodic

Semantic

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

10.3 Automatization: Creating implicit memories

Now that we have understood why problem solving is hard, and how different types of memories play a role when solving problems, let’s explore how you can improve your problem-solving skills in two ways. The first technique is automatization. Once you have practiced a skill so many times that you can do it without thinking about it, like walking, reading, or tying your shoelaces, we say that you have automatized this skill.

Many people have automatized day-to-day skills like driving or biking, but also domain-specific skills like math. For example, you might have learned to factor out equations like x2 + y2 + 2xy. If you are one of the people who has automatized the distributive law, you can immediately translate this formula to (x + y)2, with no more effort than reading this line. The important thing here is that being able to factor out equations without effort allows you to perform more complex calculations. I often think of automatization as a process that unlocks new skills in a game. Once you can double jump in a game, it allows you to reach areas of a level you could not reach before.

For example, once you can factor out equations with great ease, you can look at an equation like the one that follows and immediately see that the answer is (x + y). If you have not automatized factoring out, this problem will be a lot harder to do, if not impossible.

CH10_UN01_Hermans2

So, automatization of programming skills is key to being able to solve larger and more complex problems. But how do you reach the point of having automatized skills?

To know how to automatize skills, we first need to explore how to strengthen your implicit programming memories. Earlier in the chapter, we discussed the fact that sometimes implicit memories can get in the way when moving to a new programming language, for example, when you keep inserting curly brackets into a Python problem. While you might think that is a small mistake, it does cause some cognitive load. As covered in chapter 9, cognitive load indicates how busy or full your brain is. When you experience too much cognitive load, thinking can become really hard. The interesting thing about implicit memories is that when you have trained implicit memories well enough, it takes your brain hardly any energy to use them. For example, when you know how to bike, or how to touch type, you can do it without any effort. Because these tasks create hardly any cognitive load, you can bike and also eat an ice cream or drive a car while talking.

10.3.1 Implicit memories over time

The more implicit memories you have for programming, the easier it will be to solve larger problems because you will have more cognitive load to spare. How do you create more implicit memories? To understand that, we have to dive into how they are created in the brain.

In chapter 4 we covered how to create memories, for example, by using a deck of flashcards on which you write facts you want to remember and revisiting those cards often. However, these types of techniques are mainly useful for declarative knowledge. Facts stored in your explicit memory need your explicit attention to be stored. For example, it probably took you some time to memorize that a for-loop in Java is written for (i = 0; i < n; i++){}, and you knew that you really wanted to learn it. This is why we also call it explicit memory; it needs your explicit attention to be stored.

Implicit memories, on the other hand, are created in a different way: by repetition. When you were a kid, you tried to eat soup with a spoon many times, and after a while you knew how to do it. The memory of how to do it was created though practice rather than thinking. That is why we call this implicit memory. Implicit memories are formed in three different phases, as illustrated in figure 10.4.

Cognitive Phase

First, someone who is learning something new is in the cognitive phase. In this phase, a new piece of information needs to be split into smaller parts and you have to explicitly think about the task at hand.

For example, when you learned to index a list that is zero-based, you probably needed to spend energy to keep track of that index, as shown in the left-most part of figure 10.4. In the cognitive phase, schemata are formed or updated. For example, when you learned to index a list starting at zero, you already had a schema saved in your brain for counting outside of programming. That schema dictated that counting starts at one and needed to be adapted to also include the possibility of starting at zero.

CH10_F04_Hermans2

Figure 10.4 The three phases of storing information: cognitive phase, associative phase, and autonomous phase

Associative Phase

The associative phase is next. In this phase you need to actively repeat the new information until patterns of response emerge. You see an opening bracket and get nervous if you do not see the closing one. You might also realize that typing both brackets is a great strategy to not forget the closing one. In other words, effective actions are remembered and ineffective actions are discarded.

The harder a task is, the longer it takes to complete the associative phase. Easier facts or tasks are more quickly remembered. In the example of counting at zero, after a while you might realize that you can simply think of the element you want to retrieve and subtract 1 from it to get to the right index.

Autonomous Phase

Finally, you reach the autonomous phase (also called the procedural phase), where the skill is perfected. For example, you have reached the autonomous phase when you index into a list and you always do it correctly, no matter the context, the data type, or the operations on the list. When you see a list and a list operation, you can now tell its number immediately, without relying on counting or thinking explicitly.

Once you reach the autonomous phase, we say that you have automatized the skill. You can perform the task without any effort, and performing the skill will not add to the cognitive load a problem poses.

To experience the power of automatizing, look at exercise 10.3. If you are an experienced Java programmer, you can probably fill in the blanks in the code without thinking about it. The pattern of a for-loop is so well known you can complete it without thinking about the bounds, even in a slightly more uncommon situation like a reverse loop.

EXERCISE 10.3 Finish these Java programs by completing the missing pieces on __ as quickly possible:

for (int i = ; __ <= 10; i = i + 1) {
  System.out.println(i);
}
 
public class FizzBuzz {
    public static void main(String[] args) {
        for (int number = 1; number <= 100; __++) {
            if (number % 15 == 0) {
                System.out.println("FizzBuzz");
            } else if (number % 3 == 0) {
                System.out.println("Fizz");
            } else if (number % 5 == 0) {
                System.out.println("Buzz");
            } else {
                System.out.println(number);
            }
        }
    }
}
 
    public printReverseWords(String[] args) {
        for (String line : lines) {
            String[] words = line.split("\\s");
            for (int i = words.length - 1; i >= 0; i__)
                System.out.printf("%s ", words[i]);
            System.out.println();
        }
    }

10.3.2 Why automatization will make you program quicker

By creating a large repository of techniques (skeptics might call them tricks), we can create an ever-growing toolbox of new techniques. Gordon Logan, an American psychologist, argues that automatization is done by retrieving memories from the episodic part of the LTM, in which regular memories about daily life are also stored. Executing a task like factoring out an equation or reading a letter creates a new memory, which is an instance of a memory about the task. Because each memory is seen as an instance of an abstract class, like the class “memories about factoring,” the theory is called the instance theory.

When you are confronted with a similar task, rather than reasoning about the task—as someone might do who lacks enough instance memories—you can remember how you did it before and apply the same method. According to Logan, automatization is complete when you fully rely on episodic memory and do not use any reasoning at all. This automatic performance of tasks is quick and effortless because retrieval from memory is faster than actively thinking of the task at hand and can be done with little or no conscious attention. If you have fully automatized a task, you will also feel no need to go back and check your work, which you might be tempted to do when completing a task by reasoning.

For many of the skills you need for programming and problem solving, you have likely reached the autonomous phase by now. Writing a for-loop, indexing into a list, or creating a class are skills that you have likely automatized. As such, these tasks do not add to your cognitive load while programming.

Depending on your experience and skills, however, there are likely tasks you are still struggling with. As I mentioned before, I personally struggled with the for-loop when I started to learn Python. It was not that I could not remember the for syntax; I had even used flashcards to practice the syntax! However, often when I tried to type the words, for each came out of my fingers rather than for. My implicit memories needed to be rewired. Before we dive into techniques to rewire your memories, it would be a great idea to diagnose your own skills so that you know where to improve.

Exercise 10.4 Start a new programming session while thinking about the tasks or skills you are using while programming. For each skill, examine at what level you have automatized the skill or task and write the results in the following table. These questions can help you decide the level of your skills:

Task or skill

Cognitive

Associative

Autonomous

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

10.3.3 Improving implicit memories

Now that we understand the three different levels at which you can master a skill, let’s look at how you can use deliberate practice to improve skills that have not yet reached the autonomous stage. As we covered in chapter 2, the idea of deliberate practice is to use very small tasks and execute them repeatedly until you have reached perfection. For example, in sports interval training, a deliberate way to increase your speed is running, and in music, tone ladders are deliberate practice to train finger placement.

In programming, deliberate practice is not commonly used. When you are struggling with creating for-loops without errors, deliberately typing 100 for-loops is not something commonly done in programming culture. However, building these small skills will help you solve larger problems with greater ease because it frees up cognitive load for those larger problems.

Executing deliberate practice can be done in different ways. First, you can write a lot of similar but different programs for which you need the skill you want to practice. If you are practicing for-loops, for example, write many different forms of for-loops: forward, backward, using a stepper variable with different steps, and so on.

When you are struggling with a more complex programming concept, you can also consider adapting programs rather than writing them from scratch. Adapting programs helps you focus on how new concepts differ from those you already know. For example, if you are struggling with list comprehensions, you can first write many different programs that use loops instead. You subsequently take the programs and adapt the code until they use list comprehensions. It can also work well to revert the changes in the code manually to reflect on the difference from a different perspective. By comparing the different forms of code actively, the equivalence of the programming concepts is strengthened in your memory, as we saw in chapter 3 with the use of flashcards.

Just like with flashcards, spaced repetition is key to learning. Set some time aside every day to practice and continue until you can consistently perform the tasks without any effort. I want to stress again how uncommon this technique is in programming, so it might feel weird, but keep trying. It is really like weight lifting; each repetition makes you a little bit stronger.

10.4 Learning from code and its explanation

In this chapter we have seen that there is no such thing as generic problem-solving skills and that simply doing a lot programming is unlikely to make you a better problem solver. We have also seen that you can use deliberate practice to improve small programming skills. While mastering small skills at the autonomous level is needed, it is not sufficient to solve larger problems.

A second technique you can use to improve your problem-solving skills is to deliberately study how others have solved problems. Solutions of how other people have solved problems are often called worked examples.

Australian professor John Sweller, who also introduced the idea of cognitive load, covered in chapter 4, has extensively researched the importance of domain-specific strategies for problem-solving abilities.

Sweller taught children mathematics by having them solve algebra equations, but soon he grew frustrated by how little they were learning from only working on traditional algebra problems. At that point, Sweller got interested in experimenting with the way problem solving is taught. To gain more insight, Sweller performed a series of experiments in the 1980s. In these experiments, Sweller studied 20 ninth graders (children aged 14-15) in an Australian high school. He divided the students into two groups, who were both asked to solve typical algebra equations, such as “a = 7 - 4a; solve for a.”

However, there was a difference between the groups, as illustrated in figure 10.5. Both groups solved the same algebra equations, but group 2 simply solved the equations without further help, much like you probably also solved algebra equations in high school. Group 1 received the algebra equations but also received worked examples of the equations, which explain how to solve the problems in detail. You can think of worked examples as recipes that describe in detail the steps needed to solve the equations.

CH10_F05_Hermans2

Figure 10.5 Both groups of children solved the same algebra problems, but the left group received recipes, called worked examples, which tell the students how to solve the problem step by step.

After both groups of children finished the equations, Sweller and Cooper compared how the students performed. It probably does not come as a surprise that the first group did better; after all, they got recipes to solve the problems. Group 1 did spectacularly better; they solved the equations five times faster than group 2.

Sweller and Cooper also tested the performance of both groups on different problems because that is what makes some people reluctant to teach with these recipes; the idea that children will only be able to blindly follow the recipes and don’t really learn anything. And here is the kicker: the children in group 1 also performed better on different problems, for which calculation rules could be used (which were present in the recipe), like subtracting the same value from both sides of an equation or dividing both sides of the equation by the same.

The worked example effect has been replicated in many studies for different age groups and subjects, including mathematics, music, chess, sports, and programming.

10.4.1 A new type of cognitive load: Germane load

To many professional programmers, Sweller’s results might have been surprising. We often assume that if we want children to be good problem solvers, we should let them solve problems; if we want to be good programmers, we should program a lot. However, Sweller’s results seem to show that is not the case, and we will now dive into the details of why that is true. Why did the group who received the recipes do better than the group who had to solve the problems by themselves? The explanation Sweller offers relates to cognitive load of the working memory.

We have learned these things: the working memory is the STM applied to a given problem. Thus, while the STM can only hold two to six items, as explained in chapter 2, the working memory can only process about two to six slots available to store information.

We have also seen that when the working memory is full, it cannot properly think. There is a second thing the brain cannot do when the working memory is too full: store information back to the LTM. This is illustrated in figure 10.6.

CH10_F06_Hermans2

Figure 10.6 Germane load is needed to enable the highlighted arrow and form the working memory into the LTM. If the working memory is working too hard (that is, experiencing too much load) no information can be stored.

We’ve reviewed two types of cognitive load: intrinsic load caused by the problem itself and extraneous load caused by the phrasing of the problem. But there is a third type of cognitive load we have not covered: germane cognitive load.

Germane load, which means something like relevant load, is the effort it takes your brain to store information back to the LTM. When all the cognitive load you have room for is filled with intrinsic and extraneous load, there is no room left for germane load; in other words, you cannot remember the problems you have solved and their solutions.

This is why sometimes after a heavy coding session you are unable to remember what you did. Your brain was so engaged that it could not store the solutions.

Now that you are familiar with the three types of cognitive load, including germane load, we can reconsider the Australian experiment with the ninth graders. We now understand why the group who used the recipes did better at new equations: because their cognitive load was not that high, they could reflect on and remember these recipes. They learned that when solving an algebra problem, it might be a good idea to move part of the equation to the other side of the equal sign, which requires turning a plus into a minus, or that they can always divide both sides by the same values.

The type of skills students learned from the recipes are exactly the type of approaches that come in handy in almost all algebra problems, and thus the first group could also apply them to the new equations. The second group, while engaged in deep thinking, was more focused on the problem at hand rather than on generic rules.

The people who worry about teaching recipes have it backward. I agree, it sounds so sensible: if we want children to be problem solvers, they should solve problems. This same line of thinking is present in the programming community: if you want to be a better programmer, program a lot! Have side projects and just try out things and you will learn. But that seems to not be true.

Although Sweller’s experiments focused on math teaching, similar experiments have been done for programming. Those studies showed similar results: kids learn more from reading programs, plus an accompanying explanation, than from programming.1 As Dutch psychologist Paul Kirschner says, “You don’t become an expert by doing expert things.”

10.4.2 Using worked examples in your working life

We have seen that explicitly studying code, and studying the process of how it was created, can help you strengthen your programming skills. There are a number of sources you can use when you study code.

Collaborate with a colleague

First, you don’t have to study code alone; it is more useful to do it with someone. You could start a code reading club at work with other colleagues who are interested in studying code. If you do it together, it will be easier to keep up the habit of reading code regularly (https://code-reading.org/). If you are in a club, you can exchange code and its explanation and learn from each other.

In chapter 5, we covered techniques to understand code, including making summaries of code. Those summaries can serve as the explanation to use when reading code. You can study your own code and summaries, but it is even more powerful if you use a two-step process where you first write a summary for code you wrote, and then exchange it to learn from the code of a colleague.

Explore GitHub

If you are exploring the practice of code reading alone, luckily there is a plethora of source code and documentation online. GitHub, for example, is an excellent place to start reading code. Just go to the code of a repository you know somewhat, for example a library you use, and read the code. It is best if you choose a repository in which the domain is at least a bit familiar to you, so there are not too many unfamiliar words and concepts causing additional extraneous load, and you can focus on the programming itself.

Read books or blog posts about source code

There are many blog posts that describe how people solved a certain programming problem, and these too can serve as study tools. And although there are not many, there are a few books that also describe code and its explanation; for example, the two volumes of The Architecture of Open Source Systems by Amy Brown and Greg Wilson, or 500 Lines or Less by Amy Brown and Michael DiBernardo.

Summary


1. Marcia C. Linn and Michael J. Clancy, “The case for case studies of programming problems,” Communications of the ACM, vol. 35, no. 3, 1992, https://dl.acm.org/doi/10.1145/131295.131301.