Techniques For Improving Your Coding Skills

The other day, I came across a tech newsletter where the author challenges people to solve a coding problem in the “interview question of the week” section.

Here’s an example:

You’re given two integer arrays (n and m), and an integer k. Using the digits from n and m, return the largest number you can of length k.

Example:

n = [3,4,6,5]
m = [9,0,2,5,8,3]
k = 5
$ maxNum(n, m, k)
$ 98655

The following week, the author linked to 42 responses, and praised them all equally with “awesome work.”

Looking through the submissions, I noticed that most made the same assumptions, some were incorrect, and others differed quite a bit in the implementation. There are many ways to solve a problem with code, but they are not equal, even if the end result is the same.

Take these two JavaScript solutions. Which one is more readable, i.e., easiest to understand in the least amount of time?

Solution #1

function largestNumberOfLength(k = 0, arrays = []) {
  let output = "";
  var combined = [].concat.apply([], arrays);

  for (var i = 0; i < k; i++) {
    let highestValueIndex = getHighestValueIndex(combined);
    output = output + combined[highestValueIndex].toString();
    combined.splice(highestValueIndex, 1);
  }

  return output;
}

function getHighestValueIndex(arr) {
  if (!arr.length) return -1;

  let maxValue = Math.max(...arr);
  return arr.indexOf(maxValue);
}

Solution #2

function maxNum(n, m, k) {
  return [...n, ...m]
    .sort((a, z) => z - a)
    .slice(0, k)
    .join('')
}

Readability

Why do we care about readability? Because developers spend a lot more time reading code than writing it. We write code to communicate with other developers and with our future selves, and we want to be respectful of our time.

In the preface to the first edition of Structure and Interpretation of Computer Programs, Harold Abelson, Gerald Jay Sussman, and Julie Sussman wrote:

we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute.

A computer doesn’t care how you name variables and functions, or how you organize your code, but the people reading the code care very much. Code that is hard to read can affect your business and your end users as well, because it takes longer to fix bugs and implement features.

Some people might propose a more complex solution with the argument that it runs faster. In most cases though (for typical web applications), the speed gain does not outweigh the loss of readability. I would recommend defaulting to readable code, and if you can show with real numbers that there’s a significant performance impact, then you can consider the less readable solution.

There’s also a scientific argument for readability. An fMRI study found that “the code’s textual size drives cognitive load due to programmers’ expected attention and that vocabulary size of code particularly burdens programmers’ working memory.”

Because self-control and cognitive effort are both forms of mental work that draw from the same pool of resources, we are more likely to give in to a temptation or make other poor decisions after performing a mentally-challenging task.

In a study published in 1999, participants were asked to memorize either a 2-digit or a 7-digit number, then walk down a hallway to another room where they would write down the number. On the way, they were told to stop by a cart where they could choose one of two snacks: chocolate cake with cherry topping, or fruit salad. 63% of those who where given the 7-digit number chose the cake, versus 41% in the 2-digit group.

This has been replicated in other studies, such as this one on the effects of food advertising and cognitive load on food choices.

In Thinking, Fast and Slow, Daniel Kahneman suggests the impact is more widespread:

People who are cognitively busy are also more likely to make selfish choices, use sexist language, and make superficial judgements in social situations.

Now that we understand the importance of readable code, let’s talk about feedback and deliberate practice.

Feedback and Deliberate Practice

Looking through the past few issues of the newsletter with the interview questions, I noticed that the author simply links to all the submissions (including incorrect ones) without any kind of specific feedback. Just a generic “awesome work” or “great work.”

My guess is that they don’t have time to check every submission, and they want to recognize everyone who tried, perhaps as a way to motivate them to keep practicing each week.

If I were to start a similar coding challenge, I would take a different approach, based on the science of how we learn. People could be working on these challenges week after week and not get any better. In the science of expertise, the most effective way to level up is to follow what’s called Deliberate Practice, and one of the key components is feedback.

Some people seem to enjoy these challenges, and it’s possible some people try to solve them just for fun, and don’t care about feedback. However, there are certainly others who take these exercises as a way to improve their skills, or to prepare for interviews, as the “interview question of the week” title suggests.

The problem with praising all submissions equally, regardless of correctness or readability, and without specifying what is being praised, is that some people will interpret that as an endorsement of the solution, and the more well-known the author is, the more weight that praise will have.

To me, this is misleading, unhelpful, and could potentially cost someone a job offer. It’s worse than not giving any feedback at all.

Instead, I would follow Kathy Sierra’s advice in her excellent book Badass: Making Users Awesome. She describes the two key attributes of experts:

Those who become experts differ from non-experts with the same amount of experience in two main ways:

  1. Experts practice better (with activities that meet the criteria for Deliberate Practice)

  2. Experts develop deep perceptual knowledge and skills through high-quantity, high-quality exposure with feedback

In the domain of programming, the second point refers to letting the brain “discover” patterns by showing lots and lots of examples of high-quality code. However, Kathy warns:

Don’t expose them to examples of bad.

She explains further:

[the brain] learns best what it records the most. Even when we know (consciously) that we’re seeing examples of bad or beginner quality, our brain might not get the memo. Worse, our brain is not just failing to distinguish bad from good, it’s actively learning and trying to imitate those bad examples.

One way around that when showing examples of bad is to evoke an emotional response, which is why I went to great lengths to describe the harmful effects of hard-to-read code.

Kathy summarizes it nicely:

The best way to learn to spot “bad” is by learning the underlying patterns of “good”

Teach people to recognize bad/wrong/errors by developing and strengthening their recognition of good/right/correct.

Given what we’ve just learned, let’s see how we can improve this coding exercise.

Building a Coding Challenge

First, we need to choose the scope of the exercise. There are many skills we can teach, such as writing readable code, asking clarifying questions, and writing test cases.

Some interview exercises are vague on purpose, to see what kind of questions the candidate asks (if any at all), or if they just make their own assumptions. For this kind of newsletter-based exercise, that’s not practical, so we should make sure our exercise is as clear as possible.

Similarly, if we don’t have the time to review each individual response for correctness, then we should be upfront about that, and provide as many test cases as possible for people to check their own work.

Going back to the challenge I mentioned at the beginning of this article, one thing that wasn’t clear was whether the arrays could contain numbers greater than 9. Some people asked for clarification on Twitter, but the author never answered, which is understandable if they’re busy, and emphasizes the importance of writing a super clear challenge.

I went through each of the 42 responses, and compiled them into these groups based on the assumptions they made, and whether or not they returned the expected value:

That last one is interesting. Originally, the author said the correct answer is 98653, which confused lots of people, so the author revised the correct answer to 98655. My guess is that everyone who made the must-be-unique assumption were among those confused and didn’t see the revision.

Being curious, I looked up the text of the challenge in DuckDuckGo, and found two sites with the original challenge that inspired this one: GeeksForGeeks, and CareerCup. It turns out the solution for the original challenge is indeed 98653, because the relative order of the numbers in each array has to be preserved.

Now that makes the challenge a lot more difficult, so I can understand why the author left out the “relative order” bit, but I’m not sure why they left out the part about the arrays only containing the numbers 0-9.

How I Would Write This Excercise

This week’s challenge is a simplified version of a much more difficult problem that apparently was a Google interview question for Research Scientists.

You’re given two random arrays (n and m) that you can assume will only contain integers 0 through 9. You’re also given an integer k. Using the digits from n and m, return the largest integer you can of length k. You are allowed to combine the two arrays, and the initial order in the arrays does not matter. You can rearrange the digits in any way in order to obtain the largest integer.

To keep thing simple, we’ll assume k will never be greater than the total amount of digits in n and m.

Examples:

n = [3,4,6,5]
m = [9,0,2,5,8,3]
k = 5
$ largest_number(n, m, k)
$ 98655
n = [7,3,4,2,8,0]
m = [6,1,8,5,4,3]
k = 4
$ largest_number(n, m, k)
$ 8876

Make sure your solution returns an Integer!

How I Would Give Feedback

Based on Kathy Sierra’s advice, I would not link to or show any solutions that don’t meet the readability requirement. Instead, I would explain how I would approach the problem and propose the best solution I could come up with. If any of the reader submissions were as good as or better than mine, I would link to and praise those only.

For incorrect solutions, I would point out what was wrong and ask people to try again. For example, I might say something like “some of your solutions returned a String or an Array. Make sure your solution returns an Integer.”

How I Would Approach This Exercise

I would visualize the problem using physical objects, and describe it in plain English. I would imagine the two arrays are two boxes that each hold pieces of paper with a number from 0-9 written on them. If someone asked me to find the largest 5-digit number out of all the numbers in the boxes, I would follow these steps:

  1. Take out all the pieces of paper from both boxes
  2. Lay them all out in a line in front of me
  3. Sort them from highest to lowest
  4. Take or read out the first 5 numbers

To translate this into code, I would look up how to perform these steps in the language I was using. For example, “How do I combine two arrays in Ruby?” Some languages make this easier than others.

One of the reasons I love Ruby so much is because it is very readable. It comes with intuitive methods for many common operations. For example, if you wanted the sum of all integers inside an array, you can simply call, you guessed it, sum:

n = [3,4,6,5]
n.sum
=> 18

Similar methods exist in other popular languages (but not JavaScript), but let’s say you wanted the sum of all the digits of an integer. In Ruby, it’s as easy and concise as this:

12345.digits.sum
=> 15

You’ll have a hard time finding a language with that kind of readability. Here’s a list of solutions for this task in close to a hundred different languages.

Ruby also comes with a command line tool that makes it easy to try out Ruby code. It’s called irb, which stands for Interactive Ruby. You can launch it from the command line by typing irb and pressing the return key. Once there, let’s create two arrays filled with numbers:

n = [3,4,6,5]
m = [9,0,2,5,8,3]

Now let’s try combining them using an intuitive operation like the + sign, as if we were adding them together:

n + m
=> [3, 4, 6, 5, 9, 0, 2, 5, 8, 3]

That works!

Besides using a search engine, two other great ways to find out what kind of methods you can call on a particular Ruby object are: 1) calling methods on the object, and 2) reading the documentation, of course!

Here’s how methods works:

n = [3,4,6,5]
n.methods

This will print out a long list of methods that you can call on an array in Ruby. It’s a great way to discover methods and then either try them out or look them up. I myself just discovered the rotate method while writing this. I don’t think I’ve ever had to use it. Try it out and see what it does!

As for documentation, I recommend the Dash app. It makes it easy to look up documentation for many languages all in one place, and it has plugins for all kinds of code editors and productivity tools like Alfred, so you can quickly look things up.

Going back to our original exercise, now that we have both arrays combined, let’s sort them:

(n + m).sort
=> [0, 2, 3, 3, 4, 5, 5, 6, 8, 9]

We can see that it sorts in ascending order by default, but we want to reverse this order. Looking through the methods earlier, or by looking up the Array documentation, we can see there is a reverse method. Let’s try it out:

(n + m).sort.reverse
=> [9, 8, 6, 5, 5, 4, 3, 3, 2, 0]

Perfect! Now, we need to take the first 5 digits and put them together as one integer. Yes, Ruby reads like English here too:

(n + m).sort.reverse.take(5)
=> [9, 8, 6, 5, 5]

To put them together as one number, we’ll need to join them:

(n + m).sort.reverse.take(5).join
=> "98655"

Note that this returns a String, but we want an Integer, so we’ll need to convert it to an integer with to_i:

(n + m).sort.reverse.take(5).join.to_i
=> 98655

Yay, we got it! 🎉

But wait, it gets even better! Ruby can automatically sort the biggest k numbers in an array with the max method:

(n + m).max(5).join.to_i
=> 98655

Is your mind blown yet?

We can continue refactoring and make this more flexible by putting this inside a method that can take two arrays and the number length as arguments. Since we have more than one argument, we want to make the method as easy to use and read as possible by using keyword arguments:

def largest_number_from_digits_in_arrays(first_array:, second_array:, number_length:)
  (first_array + second_array).max(number_length).join.to_i
end

The advantage of using keyword arguments is that anywhere we see this method called, we would know right away what each argument means. For example, without keyword arguments, the method might be called like this:

n = [3,4,6,5]
m = [9,0,2,5,8,3]

largest_number_from_digits_in_arrays(n, m, 5)

In order to understand what 5 represents, we would have to look up the method definition. However, with keyword arguments, we have to specify the argument description when calling the method:

n = [3,4,6,5]
m = [9,0,2,5,8,3]

largest_number_from_digits_in_arrays(first_array: n, second_array: m, number_length: 5)

Now it’s clear what each argument represents without having to look up the method definition. The other advantage is that the order of the arguments no longer matters. Without keyword arguments, if you tried to pass in the number length before the arrays, it would throw an error:

def largest_number_from_digits_in_arrays(n, m, k)
  (n + m).max(k).join.to_i
end

n = [3,4,6,5]
m = [9,0,2,5,8,3]

largest_number_from_digits_in_arrays(5, n, m)
=> # TypeError (Array can't be coerced into Integer)

With keyword arguments, you can safely use any order:

def largest_number_from_digits_in_arrays(first_array:, second_array:, number_length:)
  (first_array + second_array).max(number_length).join.to_i
end

n = [3,4,6,5]
m = [9,0,2,5,8,3]

largest_number_from_digits_in_arrays(number_length: 5, second_array: m, first_array: n)
=> 98655

That does it for this exercise. I hope you learned something new, and let me know if there’s a way to further improve it.

Two sites that I’ve used before that do a great job with coding challenges are Exercism and Codewars. They both come with comprehensive tests so you can be confident that if all the tests pass, you have fully solved the problem, then you can focus on refactoring it in the most readable way. Exercism also gives you the option to be assigned to a human mentor who can give you feedback.

P.S.

I’ll mention one last thing about JavaScript because I think it’s a great teaching opportunity. Some of the solutions used .sort() instead of .sort((a, b) => b - a), which works in this case, but might shock you if you try it with numbers greater than 9:

array = [5, 6, 63, 33];
array.sort();

> Array [33, 5, 6, 63]

That’s because JavaScript sorts arrays as strings by default, which is why you have to specify how you want to sort. The other option is to use a typed array:

typedArray = new Uint8Array([5, 6, 63, 33]);
typedArray.sort();

> Uint8Array [5, 6, 33, 63]

It turns out JavaScript is full of surprises. At least enough to fit inside Gary Bernhardt’s infamous and hilarious lightning talk, Wat.