- From Problem Analysis to Data Definitions
- Signature, Purpose Statement, Header
- Functional Examples
- Function Template
- Function Definition
When I taught programming to teens with what is now called Bootstrap, we used the Design Recipe to help the students debug their own programs. We’d ask them “which step of the Design Recipe are you on?”, and get one of two answers: “I don’t know”, in which case we sent them back to step 1, or they’d tell us a step number and we’d talk through that or the prior step in more detail.
This is an effective method that I still use today, even on myself, both when writing new code and debugging existing issues. Let’s examine this in more detail.
The first step is to check your data structures and make sure they match the problem you’re trying to solve.
As a simple example, the problem statement might be: as I crawl the Web I want to track which pages I’ve seen so I don’t crawl them again. The right data structure for this is a set, but you could possible try an array (with progressively slower crawls and much more memory usage), a tree (storing everything as you go – again, slower and more memory usage), or a graph (accurately storing everything as you go). Selecting the wrong data structure gives you a shaky foundation for debugging.
We can start by checking the comment describing the method. Try writing one if you don’t have one already – express what you think the method does in your own words. This helps solidify the idea more conceptually. For style points, do this based on the problem statement and not the buggy code you had written.
Next up, check your types. This goes for you using a static type checker, too! Are you passing the right values to it? Do you handle the null value that it returns? Are you passing in all the data that you need in accordance with the purpose statement?
This is a great time to check on your automated tests. Make sure they capture everything described in your purpose statement, at minimum.
Try to write a test for the actual bug. This has a number of benefits in itself, plus it helps with debugging. An automated test can help prevent the bug from recurring. Writing the test can inspire other tests – if you notice that you’re testing on bogus input like null you might want to test on other bogus input like a blank string or the wrong number.
Writing a test for the actual bug also gives you confidence that your fix will work, and in part this comes from forcing yourself to understand enough of the problem to write the test. It clarifies and reinforces your understanding of the bug.
The HtDP Function Template is a neat idea that points out that, given a data
structure, you can write most of your function without even knowing what the
function is supposed to do. For example, if you have a binary tree you know you
need to do something different based on whether it’s a leaf or a node, and you
know a leaf needs a value and a node has a left and right. You don’t need to
know what the
??? operation is just yet:
def arbitrary_tree_function(a_tree) if a_tree.leaf? ??? a_tree.value elsif a_tree.node? arbitrary_tree_function(a_tree.left) ??? arbitrary_tree_function(a_tree.right) end end
This pertains to debugging, too: you can look for issues in a function without even knowing what the function is supposed to do. Is there a branch you’re not considering? Do you have an enum that should have a case statement (and do you need a default clause)? Are you ignoring a value?
At long last, let’s look at the actual implementation. This uses both the purpose statement and the examples (the tests). Does the implementation match the purpose statement? Does it actually do what the tests say it should?
Debugging at this point might be the most creative and challenging part, so I recommend looking elsewhere in our debugging posts for advice, but the important thing is that you know it’s something specific to your problem. The types are correct, the function’s structure is right, the tests are in place, you understand the problem well enough to rephrase it in your own words, the data structure is right. You are now fighting a much simpler battle than you were before you were sure of this.
If you still can’t get it, try to leave a (perhaps commented out) failing test for the next developer. Just writing this might give you one last creative idea and bug fix; even if not, it might give someone else the spark they need to uncover the solution.
If you have a failing test that you’re leaving for the next developer to figure
out, your testing framework might have a “this is supposed to fail” mechanism,
such as RSpec’s
pending. This way future developers will discover when
they accidentally make it pass.
The simple Design Recipe checklist can become as comfortable as the keys-wallet-phone pocket check you do before you leave the house, and gives you a launching point for evaluating your code and asking for help on the right points. Give it a try the next time you’re stuck!
This post is part of our ongoing Debugging Series 2021 and couldn’t have been accomplished without the wonderful insights from interviews with the following people:
- Adam Sharp
- Eebs Kobeissi
- Eric Bailey
- John Schoeman
- Mike Burns
- Rick Gorman
- Sally Hall
- Sam Kapila
- Sarah Dawson
- Sean Doyle
- One of the authors of the How to Design Programs book has since become a controversial figure with views about DEI that go against thoughtbot’s views. The ideas in this work come from a time when he was more silent about such issues. The three other authors have been fully supportive of DEI efforts in computer science and the industry. ↩︎