Coding interviews – rad or fad?

As a developer, you probably have gone through one or more coding interviews. When applying for a developer job, you are expected to demonstrate some level of programming chops by solving algorithmic problems on a whiteboard or in an online code editor. This practice is not new. My first coding interview took place more than 25 years ago sitting at a lovely green text terminal. However, it has recently proliferated and became an integral and often time-consuming part of the interview process. Especially big American tech companies like Amazon, Apple, Facebook, Google and Microsoft are pushing the envelope. To land a developer job at one of these companies, you are now expected to be able to recite the finer points of an algorithm manual and produce solutions with O log(n) time complexity in a trice.

Does that sound just slightly ridiculous to you? Perhaps it is. The question one must ask is whether having candidates produce algorithms is actually a good indicator for success in the software development profession. I have been sitting at both ends of the interviewer’s table many times, and I am quite ready to doubt this implication. There isn’t much empirical evidence to support the thesis that people who score high on coding interviews also do well in their jobs. Actually, there is none. Even Google’s own study concluded that interview scores don’t correlate with job performance. Gayle Laakmann Mc Dowell, the author of the book “Cracking the Coding Interview” says: “No one really knows, but it’s very reasonable to assume that there is a link,” and “Absence of evidence isn’t evidence of absence.”

Sorry, that’s not good enough for me. Let’s be a little more scientific.

Perhaps we begin by determining what is actually measured by solving an algorithmic problem. The coding interview is akin to a school exam. It measures how good you are at applying textbook solutions to a given task. Just as in an exam situation, you can improve your score by studying hard. The domain is broad enough to provide sufficient variation in different fields from number theory and graph theory to combinatorics and set theory. Yet, algorithms themselves are very narrow as they deal with a specific solution to a specific problem. A successful coding interview demonstrates three things: 1. the candidate can write code, 2. the candidate has a grasp of algorithms and can memorize textbook solutions, 3. the candidate can reproduce and explain these solutions.

At first glance, this seems useful. These traits are surely expedient in typical development work. They’re kind of expected from developers. But does the coding interview also measure how well pronounced these traits in a particular applicant are? Can standardised interviews establish a framework for comparison? The answer is sadly no. One applicant might receive a familiar problem that he can solve and pass with flying colours. Another more talented applicant might have never before encountered the same problem and fails to solve it. A third applicant may get bogged down by anxiety. Yet another applicant may solve the problem, but produces bugs because of using an unfamiliar language. The result is influenced by many factors and it is notoriously unreliable.

This is not the only problem. There is a more serious one: the coding interview only probes a very narrow set of skills. Being able to produce algorithms is not sufficient to become a successful developer. For example, a developer also has to read and interpret requirements, communicate within a team and with business people, have knowledge of technologies and best practices, perhaps also have an understanding of certain industries. In this light, being able to memorise textbook algorithms seems somewhat less relevant. After all, you can always look up a manual when in need of an optimised algorithm for a specific problem. This is probably what most developers would do in their day-to-day work.

I remember a peculiar coding interview that took place 10 years ago on the phone before shared online code editors became a thing. I was asked to spell out an algorithm that produces the series of prime numbers. “No problem,” I said, “just let me google the sieve of Eratosthenes.” “No no, let’s just improvise,” said the interviewer, “it doesn’t have to be perfect.” So I scribbled down some similar, yet doubtlessly inferior code that mimicked Eratosthenes’ idea and read it out loud (awkwardly) on the phone. Something I would never do in an actual work situation. Yet, I passed the interview and got the assignment.

Is this a good predictor of professional performance? No, it isn’t. It is way too arbitrary and readily produces false negatives as well as false positives. There is a considerable amount of HR management literature that has identified good and bad predictors for work performance. Among the worst predictors are first impressions, school grades, and brain teaser questions, such as “how many golf balls can you fit into a phone booth?” Finding the answer to that is a lot like devising an algorithm for a geometry problem. You get the clue. Case in point: Max Howell, the author of the popular “Homebrew” package manager for macOS was (infamously) rejected in a Google interview, because he was not able to produce an algorithm for inverting a binary tree. Howell’s software is used by millions of Mac users and is now maintained by a team of 21 people. Yet, he wasn’t good enough for Google.

One of the best predictors for future job performance is past performance. Duh. It’s kind of obvious and also backed up by evidence in the form of empirical studies in management. Had Google considered past performance instead of algorithms, the company would now be richer by one accomplished engineer, who according to his own words cares more about user experience than about computer science. People often have special talents that evade the standardised interview process. They will not escape an experienced interviewer, however. The only problem with past performance is that data is not always available. For example, what about grad students, career changers, and people with little work experience? Well, the second most reliable predictor for job performance is cognitive ability, also known as “g factor” or general intelligence. The literature is again quite solid on this point.

It stands to reason that a standardised and culturally neutral IQ test provides a much better predictor for future performance. And by “much better” I mean an order of magnitude better. It is also less time-consuming than going through a series of algorithm/coding problems. So why are companies not using them? One reason is that in certain countries, such as the USA, IQ tests are seen as potentially discriminatory and could therefore be legally contestable. This is obviously a matter of policy, and well idiocy. Encountering an IQ test in a job interview appears to raise more eyebrows than encountering a esoteric programming problems. Another reason is that high quality standardised and culturally neutral IQ tests are somewhat difficult to develop. And new ones would have to be developed all the time to ensure high quality results and to prevent abuse. Yet another predictor for career success was recently (2013) suggested by Angela Lee Duckworth in a TED talk. She called it “grit”, or the “power of passion and perseverance” and subsequently received a lot of attention from the tech industry. The problem with “grit” is that as of now no reliable way of measuring it has been devised.

So, the question that springs to mind is: why do companies use coding interviews in the selection process if they already know that the resulting score is a poor predictor for career success. Frankly, I don’t know. If you’re a big tech HR manager, please comment and enlighten us. I can only speculate. Companies like Facebook, Google or Apple are swamped with applications and might have to create artificial barriers to filter applications and reduce the number of candidates to a manageable amount for round two. What does this artificial barrier achieve? Perhaps it is about grit. Only the ambitious candidates will pass, namely the ones that are willing to learn and memorise algorithm textbooks just for the interview, and just for getting the chance to land a job with that big tech company. It is reasonable to assume that someone who has the motivation to put in three or four weeks of cramming for an interview, is also motivated to put in hard work into their job. In this case, developer interviews could just as well require candidates to conjugate Latin declinations and cite Roman philosophers. The effect would be the same, although it would certainly raise many eyebrows.

Another plausible explanation is that companies nowadays tend to put processes over people. The decision making process, especially in large enterprises, is highly formalised. Instead of leaving the hiring decision to an experienced HR manager who relies on their personal knowledge and skills, it is relegated to an impersonal procedure that produces data. On one hand, this frees decision makers from personal accountability. On the other hand it provides an instrument that top management can manipulate at will. For example, if management should want to steer their engineering staff into a new direction, it could replace algorithm questions with machine learning questions. Whatever the reasons may be, we can be sure that thousands of smaller tech companies will follow suit and imitate the example of Google, Microsoft and others, even if they don’t understand their reasons and even if the adopted procedures aren’t fitting their own true needs.

In the end, I believe coding interviews are not totally useless. They are an effective means to engage the candidate in his field and simulate a work situation. The coding interview gives the interviewer a chance to observe how a candidate approaches technical problems. For example, it may reveal whether the candidate’s course of action is methodical, or whether they have exaggerated their familiarity with a certain technology. Under ideal circumstances, it can reveal a lot about how a developer thinks. However, since an interview is a special situation these indicators need to be evaluated carefully. For the benefit of all involved parties, the coding interview is best conducted in a casual way. An expanded problem space leaves the candidate with more choices and is is likely to provide more information. I don’t see any good reason, why coding interviews should be conducted like a university exam, however. If you can think of any, please let us know.

Fixing a broken UEFI Grub boot loader

I recently installed Arch Linux on a new Dell Laptop with hybrid GPUs and UEFI. The firmware did not allow switching to legacy MBR boot mode, which I normally prefer because it is easier to install, so I had no choice in this case. To aggravate things, the newly created UEFI partition wasn’t recognised by the firmware and the computer didn’t boot. If you are in a similar situation, the following instructions might help.

I assume that you have created an UEFI partition of at least 300 MB using type “EFI SYSTEM” and GUID partition table on /dev/sdb1. Your actual device may differ, for example it could be /dev/sda1 or /dev/sda2. I also assume that you have mounted the the UEFI partition as follows:

# mkdir /boot/efi
# mount /dev/sdb1 /boot/efi

Furthermore, I assume that you have installed Grub successfully with:

# grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot/efi
# grub-mkconfig -o /boot/grub/grub.cfg

Normally this should leave you with a bootable system. In my case, however, I had old “ghost” bootloader entries left which were invalidated by recreating the UEFI partition. I first had to manually remove these. For this purpose, I used efibootmgr. Invoking the command without parameters shows a list of boot loader entries. For example:

# efibootmgr
BootCurrent: 0007
Timeout: 0 seconds
BootOrder: 0002,0003,0004,0005,0006,0007
Boot0000* grub_arch
Boot0001* GRUB
Boot0002* Preinstalled
Boot0003* Diskette Drive
Boot0004* USB Storage Device
Boot0005* CD/DVD/CD-RW Drive
Boot0006* Onboard NIC
Boot0007* UEFI: SK hynix SC311 SATA 256GB, Partition 1

Let’s assume that 0002 is an invalid entry. You can delete it with:

# efibootmgr -b 2 -B

Not that 2 is written without preceding zeros. You can also change the boot order or activate and deactivate single boot loaders with the same command. If this should not work, it can also be done using the firmware user interface in most cases.

Before I got my system to boot from my SSD, I executed two more steps to help the computer locating the Grub bootloader. First, I copied the bootloader itself to an alternative location. Keep in mind that /boot/efi still refers to the mounted UEFI partition (/dev/sdb1 in my case).

# mkdir /boot/efi/EFI/BOOT
# cp /boot/efi/EFI/GRUB/grubx64.efi /boot/efi/EFI/BOOT/BOOTX64.EFI

Second, I created a boot startup script:

# vi /boot/efi/startup.nsh

Of course, you can use another editor such as nano if you prefer. The script contains only one line:

bcf boot add 1 fs0:\EFI\GRUB\grubx64.efi “Grub Bootloader”

After saving, unmounting, and rebooting the system, I was able to boot from the SSD.

Getting Linux rolling

A few weeks ago I upgraded my Ubuntu from 16.04 to 18.04.1. I wanted to do the upgrade earlier, but reports of compatibility issues kept me waiting for the first maintenance release. The upgrade went trouble-free as expected. I think Canonical did a great job on the installer and the Gnome customisations. However, as with previous Ubuntu upgrades, there were quite a few post-installation issues.

The problem I noticed first was that the update had somehow jumbled the hotkey mapping. OK, no problem to fix that manually. Next, I couldn’t connect to the MySQL server any longer as the update had replaced the config file. This was also not a big deal, because the update process saves all existing configs. I simply had to restore the relevant lines. A bit trickier was the PHP installation. It seems that the old 7.0 package was left intact and the 7.2 version was installed only partly by the upgrade. I am not able to add modules to the 7.0 package any longer, since the PPA repositories changed.

I also encountered a few UI problems with scrolling and text pasting. For a week, I could not scroll terminal output back until I found a fix for this problem. Copying and pasting text is very slow sometimes. Could have to do with the switch from Unity to the Gnome shell. I wasn’t able to figure it out yet. All in all, a fresh installation would have been cleaner and less troublesome. However, I don’t want to go through that, as it would force me to reinstall all applications and reconfigure my Docker-based development setup, which surely takes more than a day.

With Ubuntu, or Debian, or in fact any other non-rolling Linux distro, major updates are released at least once a year. Even with LTS releases, you have to update every two years. Most packages are quite outdated at that time. In software development, we are striving to shorten release cycles ideally to a continuous deployment model. Therefore, it becomes more important to keep libraries and tools up-to-date. Simultaneously, deployments and tool chains are increasing in complexity. Hence, reinstalling environments from scratch becomes more cumbersome.

For these reasons, I decided to migrate to a rolling Linux distribution. Ubuntu is great and I really like the ease of installation and the fact that it is stable and well supported. But perhaps it’s time to try out something new. The obvious choice for me would be Arch Linux, so I installed Arch and a few of its derivates in virtual machines to get a feel for it. I am going to pick one of them and maintain the Vbox for a while before installing it on bare metal. As of now, I am not sure whether Arch is stable enough to function as a primary environment for daily work.

The Arch Linux base distro is certainly special. Its installation process is entirely manual and the resulting image is quite Spartan. You have to bootstrap everything by hand and even for someone experienced it takes a few hours until everything, including a graphical desktop environment, is configured and running. The advantage is that the system is completely customisable and can be configured exactly to your needs. Kind of nice.

I’ve also tried Manjaro and Antergos, both Arch-based distributions. They are significantly easier to install and also provide more convenience for configuration. For example, Manjaro has a nice graphical tool for managing kernel versions and Antergos offers six or seven different desktop environments out of the box. Like Arch, they are based on the Pacman installer. Although there are only about 11,000 packages in the Arch repository, the AUR (Arch User Repository) adds another 47,000 packages which is on par with the biggest Linux distributions.

The code is the documentation

The first time I heard someone saying: “the code is the documentation”, I thought it sounded completely wrong, like a lazy excuse for not producing documentation. However, it kept me thinking and I realised that there is also truth in this statement. This paradoxical thought-provoking quality makes it a proper mantra for agile practitioners, because it expresses a fundamental agile value: “working software over comprehensive documentation”.

Before we go further into details, I should dismiss the notion that agile developers don’t write documentation or misconceive its worth. Nope. We still produce documentation. However, we also apply the following agile principle to it: “Simplicity, the art of maximising the amount of work not done, is essential.” It is expedient to minimise documentation by adhering to practices that reduce the need for it. In agile development, documentation takes on the role of accessory parts whereas the primary attention is given to the codebase. Or more succinctly:

“Truth can only be found in one place: the code.” (Robert C. Martin, Clean Code)

The codebase is the ultimate source of truth. It is referenced in case of doubt, when a question is either too detailed or if the documentation is out-of-date. The code provides insight where no other method of reference is available. With this in mind, it is evident that code should be written in a way that is well-structured and understandable. Self-documenting code reduces or disposes the need for external documentation. Knowledge is represented in a single artefact and there is no need to synchronise multiple sources. Unfortunately, most real-world codebases do not have these ideal qualities. There could be many reasons for that, but the most common reason is that software entropy has taken its toll over time, and that too little attention was paid to refactoring.

So, code quality, self-documenting code, and continuous refactoring go hand in hand. It is important to understand that the highest code quality is achieved through conceptual clarity. Conceptual clarity comes from good naming and  good structure. This is by far more important than coding style, naming conventions, formatting and other external features, although the latter do of course contribute to code quality. Naming and structure cannot be tested automatically. Unlike code style, conventions, and formatting, they require human perception and intelligence. Which is one more reason to adopt such practices as code reviews and pair programming.

Coming back to “the code is the documentation”, I think the best way to understand this phrase is as an abstract ideal that ought to be worked towards. In an ideal world, code doesn’t need additional documentation, because it is so beautifully clear that it can be understood by anyone without any prior knowledge. It answers all questions that might arise about the software. It clarifies the intentions of the programmer and it is therefore also easy to maintain and change. Obviously, this is really difficult to achieve in the real world, especially across a large codebase, but its self-documenting properties are quite likely the best measure of code quality.

Code Review Antipatterns

Don’t you love code reviews? Having your grand solution scrutinised, criticised, and optimised by your colleagues makes you feel awkward? It doesn’t have to be that way. Code reviews are actually fun if done right and if the participants are aware of the pitfalls. They are not just conducive to achieving a high level of code quality, but they are also an ongoing learning experience.

There are three basic forms of code reviews: informal reviews (“hey, can you have a look at this?”), formal reviews (a formal task assigned to one ore more peers), or pair programming. The latter form has the advantage that it is done at the time when code is created. It is also the only form where two individuals engage in a direct conversation. There is no time lag. This makes pair programming the preferential form of review for high-priority tasks.

Agile teams working with git (or any other version control system) often encounter code reviews as a step in their code merging workflow. The typical unit of code being reviewed is a merge request, aka pull request in this setting. So, here are a number of things to avoid when doing a code review.

Not doing code reviewsLet’s begin with the obvious. Four eyes (or six or eight…) see more than two. Errors, problems, vulnerabilities, and bad design gets spotted early on. Corrective measures are still inexpensive at this stage. There is a dual benefit. Code quality increases and knowledge is shared. At least two people of a team are familiar with each feature or function, namely the author and the reviewer.

Confusing code reviews with functional reviewsCode reviews focus on code. Duh! The review is primarily (but not exclusively) concerned with the non-functional properties of the software. Functional reviews, on the other hand, ensure that functional requirements are met. The latter activity is closer to testing, therefore it makes sense to perform functional reviews in a separate procedure, probably as part of testing.

Bitching, nitpicking, fault-finding, patronising – It goes without saying that treating colleagues with kindness and respect is necessary for productive collaboration. Code reviews are no exception. Offensive behaviours only derail the process, while courteous and supportive comments go a long way. The reviewer must guard against coming across as a know-it-all and keep office politics out of the dialogue. In written reviews, annotators should aim at being helpful without being wordy.

Discussing code style conventions – Code reviews should not be about lexical code style, formatting and programming conventions. There are automated tools for this. If you care about formatting and code style, use a linter. More importantly, decide on a set of programming style conventions and configure the linter accordingly. It is totally OK if the team agrees to leave certain options open to individual preference. The formatting and style of code that passes the linter should not be argued, because it’s simply a waste of time.

Ignoring naming – Programming conventions may or may not encompass identifier naming conventions. However, linters cannot determine whether a given variable name is good or bad. Machines don’t care about names, only humans do. Phil Karlton has described naming as one of the two hard things in computer science. By all means, do review the identifier naming in the code review. As a rule of thumb, if an identifier name leaves you puzzled the first time you read it, it is probably not well chosen.

Debate – Code reviews aren’t meant as a podium for discussion. The objective is to improve the resulting artifact, not to find out who has the better arguments. Sometimes, people do have different opinions about practices and solutions. It may be helpful to resolve these by giving examples and references to commonly accepted best practices. If that is to no avail, a third party (or fourth or fifth…) should be be involved. In a written review, consider involving a third party, whenever the dialogue drifts into debate.

Not getting a third opinion – Involving a third party into the review is not only useful in cases where two people cannot agree. See first antipattern: four eyes are better than two, and six are better than four. If an important architectural question is at stake, why not involve the whole team? See: mob programming.

Reviewing huge chunks of code – Code reviews should be limited in scope, ideally to a few pages of code that can be completely read and understood in 20 minutes. Studies have shown that thoroughness and number of spotted issues are inversely proportional to the amount of code being reviewed. Therefore, results are improved if smaller units of code are being reviewed. Including time for reflection and annotations or discussion, the entire review should not take longer than 45 minutes. Anything exceeding one hour is questionable.

Unidirectional reviewing – The general idea is that everyone should review everyone’s code. Most teams have both senior and junior developers on board or perhaps a team lead or an architect. This often leads to the situation that seniors only review juniors, but not vice versa. This is wrong. First of all, seniors can make mistakes, too. Second, juniors can benefit from reading code written by senior developers. Third, varying fields of specialisation can be leveraged. A junior developer might have special knowledge or skills in a certain narrow area.

Rewriting code – It’s called code review, not code rewrite! Rewriting someone’s code is just not appropriate in most situations. It’s akin to pair programming where the navigator just grabs the keyboard from the driver whenever he feels like it. It’s simply rude. If the reviewer has a better idea, he should annotate the merge request with code examples.

Overdoing it – Programming is not a beauty contest. Code does not need to be perfect to be functional and maintainable. For example, even though I might prefer a functional loop construct over an imperative one, it’s just fine if the imperative one works. Perfectionism sometimes gets in the way and leads to other antipatterns such as premature optimisation and presumptive features (YAGNI). Also: A piece of code written for a one-time migration or ops procedure does not require the same high standards as production system code.

Underdoing it – Being too forgiving is likewise unhelpful. If the code review is executed as a mere approval formality, it’s a waste of time. Code should always be inspected for things like security holes, logical errors, code smells, technical debt and feature creep. It is important to strike a balance between clean maintainable code and reasonable effort.

Not reviewing tests – Unit tests are often mandatory and sometimes functional and acceptance tests are also added to the artifact. These should also be reviewed to ensure that the tests are present, logically correct and meet the standards. It’s a good idea to verify an agreed upon code coverage percentage as part of the review.

Not using a diff utility – Most teams probably use a tool set with built-in diff and annotation capability. In case you don’t, you make your work harder than it has to be.