The second model is all about being able to gauge your own progress in learning a new subject. As I mentioned in the previous post, how confident you feel in a subject isn’t necessarily a reliable metric, for various reasons. We want to move away from assessing ourselves using only our gut feelings. And Bloom’s taxonomy provides a framework to do just that:
It’s a hierarchy that consists of different levels of mastery over a subject. To move up, or operate within a certain level, you need to be able to perform certain tasks or solve certain kinds of problems. As you move up to higher levels, those tasks become more complex, not just because they build on mastery over the previous levels, but because they involve “phase-shifts”.
These phase-shifts are the “aha!” moments we all experience when learning a new subject. Where stuff just clicks and all the pieces seem to come together. Where there’s a distinct “before” and “after.” A one-directional paradigm-shift, where you take a step back and can no longer see the world the same way. One nice thing about this process is that as you move up to higher vantage points, you’ll be able to reflect back and see the limitations of past perspectives. And if you can communicate how you navigated the climb up the Bloom’s pyramid, then you’ll be able to help others walk the path too. Which in turn, helps your own learning.
Recursion is a recurring theme that I write about (some pun intended). Being able to take a step back and analyze a previous state can be valuable, whether that’s while learning, or in a computer program. And at the risk of becoming a little too meta, I’ve decided that recursion would be a great example to use for this blog post to illustrate the different phases of the climb.
Remember
The first step in learning something new means being able to memorize, define, repeat, and recite. It’s how most of us learned our multiplication tables growing up, and why we used flashcards to study from multiple-choice tests. It’s basically just pattern-matching, and though it’s a shallow form of learning a subject, it’s still a necessary first step.
At this level, you’re learning basic terminology. So using our recursion example, this would be knowing that a recursive function is a function that invokes itself in its own body. It’s knowing that such a function is made up of a base and recursive cases. And it’s knowing that it’s often used in iterating through branched data structures, like trees and graphs.
Memorizing these kinds of succinct definitions can be useful in interviews in case the interviewer feels like throwing a few trivia questions your way. You want to be able to rattle off answers quickly and concisely to show your understanding. But just memorizing information isn’t enough to pass coding interviews, and that’s one of the things that make them so challenging. You have to actually know how different topics are connected to one another.
Understand
Knowing is not the same as understanding. This is more than just a semantic difference. For starters, you might look at this problem statement and know what every word means, but not understand what the problem is asking:
Program for Fibonacci numbers - GeeksforGeeks
And if you don’t understand what the interviewer is asking you, there’s almost no chance of you accidentally stumbling across the solution. It’s one of the first things I stressed to engineers while I was an instructor at Outco. And it’s not always a trivial matter to figure out. Sure, the first step is understanding inputs and outputs. But then you have to understand the roadmap of how to get where you want to go. You have to understand why it might be a challenging problem, or what inputs might break your assumptions, or why the interviewer wanted to ask it. There’s always more to unpack about problems when trying to understand them than what’s given in the problem statement.
If you want to go a little deeper down the rabbit hole, Vsauce has a good video where he breaks down the meaning of the word “understand”: He relates understanding to being inside of and surrounded by something. That something could be a set of topics, ideas, or concepts. Defining understanding this way helps explain why we use the words “immersive” to describe Outco’s interview prep program or we say we’re “diving into” a topic like recursion or dynamic programming.
So if I gave you some code to solve a recursive problem like the Nth Fibonacci Number, mastery over this level means you’d be able to identify which part of it is the base case, and which part is the recursive case. You understand why the base case goes first, and why you don’t need an else clause to your if statement. And most crucially, you understand why you might want to set this problem up as recursive function: it comes down to solving smaller versions of the same problem and combining those solutions.
To me, understanding is all about drawing connections between facts. It’s “connecting the dots.” It’s seeing how all the different pieces of the puzzle fit together. And it’s using different lenses to see the world. But understanding how the pieces fit together isn’t enough. You have to actually solve the puzzle.
Apply
In coding interviews, this is probably the most crucial step that you are being tested for. Being able to apply what you know is pretty self-explanatory. But, how can you tell if you’re actually ready to apply your knowledge and understanding? What is a clear test you can give yourself? Being able to look at the solution to a problem and identify what each part of the code does and how it works is a good indicator of being at the previous level of understanding. Being able to look at a new problem statement in a given domain and getting to a solution that can pass a set of predefined test cases is a good indicator of being at the level of applying.
It’s a tricky thing because understanding the code someone else wrote can give a false sense of mastery. It can give you the false belief that you’d be able to come up with that same solution on your own after reviewing the answer once. While it’s good to review other people’s code and to try to understand it deeply, the only way to really know if you’ve mastered a subject is to try applying it to new problems. You don’t want to just know how to solve the Nth Fibonacci number, you want to be able to solve any recursion problem that you encounter in the wild.
This creates the challenge of needing to then find new problems, that have been pre-sorted into a particular category you’re interested in. Just understanding that there even are different categories of problems presupposes a certain level of mastery. This is where the structure of a curriculum comes in handy. A good one organizes the set of all available problems into a progression of topics that build on and reinforce one another. It’s something I spent a lot of time working and iterating on while at Outco.
To some extent, mastering this level just requires doing and seeing a lot of different problems, and in different contexts: whiteboarding, timed challenges, longer-form homework problems, etc. But I think it’s valuable to note that what’s actually happening when you learn how to apply a concept like recursion, is that you’re simplifying down the essential components of that problem. You’re trying to turn it into a game of plug and play, where you have some blueprint of how to solve the problem readily available in your mind, and now it’s just a matter of filling in the blanks and accounting for the quirks of that particular problem.
All recursion problems have a base case and recursive cases. What are the conditions for each? And are we aggregating some return values at the end of the recursion or are we picking some optimal choice of return value? Does this follow a pure recursion pattern, or a helper method recursion pattern? When you’re able to apply what you know, new problems start to become instances of a single pattern with small variations. And the chaos of the world gets reduced down to a set of blueprints.
However, just because you can arrive at a solution, doesn’t mean you can’t arrive at a better solution.
Analyze
What makes one approach better than another? What are the factors that we need to consider to make that determination? Is it the number of lines of code we used? Is it the number of external libraries we reference? How do we take into account code reusability and readability? How important are things like variable names or code cleanliness and indentation? There are definite tradeoffs between cleverness and clarity:
Clear is better than clever | Hacker News
Sometimes a solution to a problem can be written in one line of code. That code will work, but is that good code? A lot of that analysis depends on the context in which that code is used. The runtime of this is the same as the first solution above, but it’s definitely not as easy to read. Someone who isn’t familiar with ternary operators or javascript arrow functions might have a hard time parsing what the hell is going on. So if I were to use this as the first solution to teach a topic like recursion, there’s a better chance of more people remaining confused and not learning the topic.
To me, analysis is all about being able to translate one way of looking at information into another useful way of looking at it. For recursive problems, it’s knowing how to diagram and extract useful meaning out of the diagram.
It’s knowing what each of the branches in the diagram represents: a function call. It’s knowing how to interpret the diagram and understanding how the call stack behaves in that context. In this case, one source of confusion people often have is that these operations are all being run in parallel, when in fact there is a sequence to them, that follows a pre-order depth-first traversal of that tree.
You’ll be expected to do some level of analysis in most of your interviews. Usually, this takes the form of analyzing the time and space complexity of your solution, or the different parts of your solution. For system design questions, it might take the shape of analyzing performance, or scalability, like how many requests per second can be handled, or what the maximum load a system will be able to take.
On the job, you’ll be expected to analyze the impact a change has on a product, through additional clickthrough rate, or revenue, or users or any other meaningful metric. But once you know how to analyze and break things down into their fundamental components, how do you use those pieces to make decisions?
Evaluate
This step is all about being able to compare different possibilities and then deciding on the best path forward. It’s all about being able to make judgments based on constraints with the goal of achieving a particular outcome.
In the context of an interview, this level of mastery helps inform your decision on which approach to take when solving a problem. Often you have multiple potential avenues to a solution, and you need to pick the best one. Not only that, but you also have to describe tradeoffs, analyze differences, weigh the impact of potential drawbacks or edge cases, and assess how all of that feeds into your decision-making. You may only have to comment on your approach briefly at the beginning and/or the end, but your response can tell the interviewer a lot.
Which is why it’s difficult to bullshit this level of mastery. Being able to evaluate requires a very deep level of awareness of the particular domain. You have to see how all the dots are connected, and navigate that web of information, to extract the usefulness out of it. Value literally comes out of the evaluation.
Which language, or framework, or backend, or database should we use? What the best architecture, or pod structure, or meeting schedule? How should we prioritize which features to build, which we support and which we decide to deprecate? And how do we justify those decisions? What are the important factors?
Do we factor in what technologies the team is already aware of? Do we spend time upfront to invest in hiring and training, or do we just make a push with the resources we have? Do we grow the team, or outsource part of this work? How much time will we save by automating feature X, vs feature Y? How much room for error or system failure do we need to tolerate? What languages should we support? What other companies should we partner with?
All of these are valuable questions, that don’t necessarily have clear answers because of our lack of omniscience. We might not accurately measure the probability of an unlikely event happening. For instance, the 2008 housing bubble or the 2001 dotcom bubble. We also might not ascribe the correct value to said event or action. For example, Kodak underestimated the value of Instagram, and Newscorp overestimated the value of MySpace. There’s a cognitive bias that explains why this happens, known as the Planning Fallacy, but it can be boiled down to this simple statement:
We tend to overestimate the benefits and underestimate the costs.
And to make matters worse, factors we didn’t account for can come out of nowhere and make an evaluation completely irrelevant. Again, we have a bias towards underestimating the chances of such events occurring and the impact they will have on us. See the Black Swan Theory:
Going back to our original example of trying to solve the Nth Fibonacci Number, we should be able to rule out the brute force recursive approach because of its exponential runtime from our previous analysis. If we understand how to implement a dynamic programming solution like memoization, we should be able to improve our solution to run in linear time. Here’s a blog post that goes into some more detail about that solution:
How to implement Memoization in 3 Simple Steps
However, there is another approach that completely does away with the recursive solution and uses tabulation to achieve the same linear runtime. Here’s what the code for that looks like:
https://gist.github.com/spiterman/33033a45e5bdfb758cd909a28d3c4431
So which one should we choose? On the surface, they seem like they would be equivalent since they have the same time and space complexity. But we have to consider some additional overhead that comes with recursive solutions, namely the call stack.
The table in the tabulation approach takes the place of the call stack, and this makes a big difference for the maximum input size your function can handle. How many function calls your machine can keep track of is something you don’t necessarily have control over and is typically capped at something less than the maximum array size you can store in memory.
Generally, if there is an iterative approach you can use over a recursive approach, you should use it. And during times you have to use recursion, you should try to optimize it with tail recursion wherever possible. Below are some resources on the subject:
The key takeaway is this: to be able to evaluate between different competing options or solutions requires a very deep mastery over the subject. It may take time to develop this mastery, but at least some level of it is required during the interview and it is essential for progressing further in your career.
Create
And at last, we‘ve reached the top of the pyramid, which Bloom considered to be the apex of mastery: creating new works. As engineers, we build things. Tools, platforms, systems, programs, frontends, backends. They all need architects, designers, and visionaries to implement. It’s hard to say where creativity comes from exactly, but it requires imagination. Specifically, the ability to imagine a world meaningfully different from the one we live in: A world with the thing you created in it.
It also requires will power to carry out the necessary steps to bring that thing into the world. Good ideas don’t just manifest themselves, they require execution. This is the importance of a portfolio during the job interview process. It’s the answer to the question, “what have you created?” They want to see, what are you capable of imagining and then willing into existence?
I’ve found that when developing creativity, it’s good to start small and simple. Because eventually creativity snowballs. Your first work most likely won’t be your best work, but most people give up too early in the process to see what their best could be. I think the biggest determining factor in whether someone will stick with something long enough to master it is whether that person finds meaning in what they are doing. It’s a much stronger motivation than material incentives:
Instrumental and intrinsic value
And to me, meaningful work is something that creates “value.” Which is a tricky concept to pin down because it extends beyond just the dollar amount someone is willing to pay for something. It can be entertainment value, or educational value or personal value, or even spiritual value. It could be writing a blog post about learning and recursion to help others understand both topics better. I think of value like energy. It can take many different shapes, but in the end, it’s all basically the same thing.
I have another post about how creativity can be further categorized based on its impact, but essentially it boils down to three levels:
Creating Something at All
Creating Something People Value
Creating Something People Use to Create Things of Value
The Creation Hierarchymedium.com
Which brings us to the end of the post, because now we’ve gotten to the point of peak meta-recursiveness, where we’re talking about creativity creating more creativity. But that’s the progression Bloom saw, and hopefully, this hierarchy can help you achieve a better awareness of your skills and your blind spots.
Hopefully, you enjoyed this post, be sure to check out the Outco program at outco.io if you’re interested in accelerating your career in software engineering, and stay tuned for the next post!
And FYI, here’s a hacky constant time workaround for finding the nth Fibonacci number:
Because sometimes the most creative thing you can do is free up time to create something else 😜.