Designing and making software is hard. One has to think at multiple levels of abstractions and connect things. Can we use shortcuts to prompt ourselves into thinking and making sure we are not missing something?
Mental models are concepts, frameworks, or ways of thinking. I like to think they are here to debug your thinking. Going through a list of mental models can help you see if you are not missing something and prompting you to use new concepts.
This article and the following ones attempt to collect and organise mental models that help you design, debug and improve code. Let’s get started with a few favourite mental models for finding solutions to problems.
How will your algorithm behave with a significant input? What is the hidden cost in the long run?
Most developers know about Big O notation when doing algorithm analysis. Using approximation and the growth rate of a function gives us a great framework to compare algorithms.
These types of back-of-the-envelope calculations can be useful in helping choose and future proof your work. However, beware of falling into the trap of premature optimisation. Remember that perfect is the enemy of good.
Bayesian thinking let you look at the world with probabilities and update your beliefs in the light of new information. On the contrary, frequentist thinking relies on and requires many observations of an event before it can become useful.
How does it work? Start by picking a hypothesis. Once you see new evidence, you can update your hypothesis. Let’s say you have a hunch about something being true, but you are not entirely sure. After a few days of observation, you can update how confident you are in the light of new observations. Humans seem to do that all the time naturally.
A practical example of Bayesian thinking is spam filtering. Knowing what kind of words are in spam, we can predict that a message is spam by looking for those words. In other words: our model predicts the likelihood of spam by looking for particular words. When we see new spam, we can update our model by adding those words — we improve our model.
One way to solve a difficult problem is by using the brute-force technique. We solve the problem by exhaustively enumerating all the possibilities. This kind of algorithm can end up doing far more work, but it is easier to implement. Sometimes simplicity is better.
Solving a problem involves finding the right combination of options. With permutations and combinations, our choices are exponential when options are abundant. You might need to resort to an approximate algorithm or use heuristics.
Many frameworks exist! React or Vue for making websites. MVC or MVVM to organise your project. Have you thought about shopping around? Reinventing the wheel is good, but relying on proven solutions can be better and make it easier to attract new developers. However, beware of falling into the trap of novelty.
The concept of design patterns was introduced by the architect Christopher Alexander as a reusable solution to a design problem. This concept made its way to software engineering thanks to the “Gang of Four” book Design Patterns: Elements of Reusable Object-Oriented Software. This book presents reusable solutions to common software design problems. Worth a read!
Beware of anti-patterns which look like great solutions but are in the end sub-optimal or lead you to bad architecture.
Is your problem too big? Do you need to solve it in parallel?
Divide the problem into smaller subproblems.
Conquer the subproblems by solving them if simple enough. If not divide them again until they are simple to solve.
Combine the solutions of the subproblems to get the solution to the main problem.
When things become hazy, it might be time to stop, breathe, and, start from first principles.
Go back to the beginning. What are you trying to solve? What is the input, what is the output? Forget about existing code and structure. What would you do if starting from scratch?
The idea is to break down complicated problems as far as you can and then think from the ground up. One great way to achieve that is to use Five Whys: ask why as many times as necessary.
Thinking from first principles is also an excellent opportunity to learn more about a subject and acquire knowledge. Have you ever dreamed of finding out how React works?
“What I cannot create, I do not understand. Know how to solve every problem that has been solved.” — Richard Feynman
Next time you are stuck on a problem where there are many combinations and permutations to test, try using heuristics.
Heuristics are shortcuts for your decision making — an educated guess or rule of thumb. Be careful choosing one as they can be inaccurate or hold false assumptions. The heuristic “Avoid green vegetables” could make you miss out on tasty options.
A Greedy algorithm is an algorithm that uses the following heuristic: solve the problem sequentially and at each step make the best local decision. It might not lead to an optimal solution, but it will be good enough.
Looking at a problem from different angles can be helpful.
A few techniques to help you change perspectives:
Your code will certainly fail.
The hypotheses that use fewer assumptions or rely on fewer dependencies are usually preferable. When in doubt, always choose the most straightforward explanation or solution as they are easier to verify and execute.
When designing a solution for your problem you might devise an elegant but complicated algorithm instead of going for the easier one. It is worth reflecting on the dependencies and cost associated with such a decision.
Some problems are simpler to solve empirically rather than theoretically. Monte Carlo methods use random simulations to find numerical answers. For example: by placing random points in a square, we can find the ratio of points falling in the circumscribed circle versus the square. This ratio is an approximation of Pi!
An imperative algorithm uses a series of commands to manipulate data. When writing code, we tend to use sequential commands with loops and iteration.
Read N pages = read page 1, read page 2, …, read page N.
A recursive algorithm is defined in terms of itself: the algorithm uses itself to solve the problem. Recursion is a top-down approach: the algorithm starts with a big problem and calls itself recursively on smaller problems. This suits Divide And Conquer strategies very well. A recursive algorithm can be translated into an imperative version.
Read N pages = read page 1, read N minus 1 pages.
With recursion, one can describe infinity with a finite algorithm. Think about how fractals display intricate, infinite and beautiful structures from the simplicity of a recursive formula.
“The journey of a thousand miles begins with a single step.” — Lao Tzu
Start with a guess. Do some computation. Compare the results to nature, experiments or empirical data. If they don’t match, the guess is wrong. Start again.
A fit guess or model can be valid for a while, but in light of new insight can become less accurate. Do not be afraid to update your model!
What if you could for a minute think the opposite or make assumptions about something? Exploring impossible situation outcomes is an exercise that can expand your knowledge. Schrödinger’s cat is a famous example of a thought experiment.
A few questions like the following can get you on the path:
Moving up or down the abstraction ladder can give you clarity. Try going from concrete to abstract or vice versa. For example, when designing an API, start with the end-user experience and walk down the details. Let the high level dictate the low level.
We just saw a few mental models that are useful to have around when solving a problem. Mental problems are a great tool, but you need to keep practising using them to get the best out of them. By practising, you will remember them and spot when one could fit. To get started, you could try using this list of mental models like a checklist. Next time I will present some great mental models for debugging.
Continue the discussion on Twitter →