Teaching the Unfortunate Parts

Posted on

When explaining a technology, we have to decide how to approach its shortcomings. There might be mistakes in its design, or it might have usability problems, or it might be unreliable. How do we approach these and how much emphasis do we place on them?

One approach is: "This tool has a lot of problems, but we'll show you how to avoid them." That can demotivate the learner: "Why am I learning this thing if it has so many problems?"

Another common approach is to never call out the problems at all. That can also demotivate the learner, but the demotivation is delayed. When they finish your video, book, etc. and use the tool in practice, they suddenly encounter the sharp edges. Now they might think "I haven't heard about these problems so this must be my fault."

In Execute Program, we try to avoid both of the above approaches. Instead, we aim for neutral descriptions in our courses. Where problems exist, we describe them flatly and directly. Often, probably due to personal writing quirks, our descriptions involve the word "unfortunate".

Here are four times when we used the word "unfortunate" or "unfortunately" in a lesson. Each is expanded here because the original surrounding lesson context is missing, but the original "unfortunate" is retained in each example.

1. NaN

(From the Modern JavaScript / isNaN lesson. The lesson links in this post, including that one, will take you directly to the interactive lesson, which doesn't require registering an account.)

Any operation on a NaN returns another NaN. Unfortunately, that means that NaNs will propagate through the system. By the time you actually see the NaN, it might have ended up in a very different place from where it started.

> const lastIndex = ['a', 'b', 'c'].elngth - 1;
const middleIndex = Math.floor(lastIndex / 2);
middleIndex;
NaN

Note the typo: elngth. That returns undefined, then we subtract 1 from it giving NaN, then we divide by 2, then we call Math.floor. Now imagine that each of those steps happened in a different function.

All we know is that a NaN came out the end. Now we have to ask the perpetual question in dynamic languages: which function (or class, or module) caused the NaN (or null, undefined, nil, None, etc.), and which functions merely propagated it?

NaN itself is part of the IEEE 754 floating point standard. It behaves the way it does for a reason. But this is still a JavaScript problem because JavaScript returns NaN in so many cases where other languages don't. In our example above, the NaN showed up when we did undefined - 1. Other dynamic languages (like Ruby or Python) throw an error when we try to do that, and most static languages prohibit it at compile time. This is a problem specific to JavaScript.

2. Mapping over maps

(From the Modern JavaScript / Maps lesson.)

Modern versions of JavaScript support a map data type. It's kind of like an object where the keys can be any type, not just strings. JavaScript also has a map method on arrays, which transforms the array's values into other values.

The data type Map and the method map are related at the conceptual level: they both "map" (or "relate") things to other things. Other than that, they have no relationship. Their identical names are just an unfortunate accident of history.

3. No negative array indexes in JavaScript

(From the JavaScript Arrays / Negative array indexes lesson.)

Ruby and Python both allow negative indexing on arrays. In those languages, arr[-1] means "the last element of the array". JavaScript has a concept of negative indexes: we can call arr.slice(-1) to get an array containing only the last element of arr.

We might expect that to work for normal array access as well, as it does in Ruby and Python: someArray[-1]. Alternately, we might expect someArray[-1] to throw an exception. Unfortunately, it doesn't do either of those. Instead, it returns undefined, the same value that we get when accessing any other index that doesn't exist.

> const arr = ['a', 'b', 'c'];
arr[-1];
undefined

4. TypeScript's unsoundness

(From the TypeScript / Type soundness lesson.)

A sound type system is one that fully enforces its own rules. TypeScript is unsound, which means that the compiler will sometimes accept code that violates the compiler's own rules.

In the example below, we create an array of strings. "Array of strings" means that it should never contain anything else. It certainly shouldn't contain any numbers.

Then we assign the array to a new variable with the type string | number. That lets us push a number into the array. We've now violated the original array variable's type: it says it's a string[], but it contains both strings and numbers.

> let names: string[] = ['Amir', 'Betty'];
let unsoundNames: (string | number)[] = names;

// This is unsound! It causes the "names" array to contain a number!
unsoundNames.push(5);
names;
['Amir', 'Betty', 5]

This is very bad, and unfortunately there's no way to fix it.

Being honest about problems

It's important to acknowledge design problems, especially when teaching. Learners are new to the topic, so they rarely have enough context to analyze designs critically. For example, someone learning TypeScript as their first static language won't know what soundness is.

People are going to hit TypeScript's unsoundness in real code. They'll find NaNs propagating through their JavaScript systems running in production. They'll wonder why the map data type and the map method have the same name even though they don't interact. Their first thought will be that they're just missing something.

As authors, we could try to head this off with "OK, this technology has a ton of problems... in fact, it's pretty bad, but here we go, let's learn it!" That sets the learner up for demotivation from the start. Alternately, we could describe only the good parts of the tool, staying silent about the sharp edges. But that sets the learner up for demotivation later when they encounter sharp edges and blame themselves.

It's better to describe the good parts, then tell the learner that, unfortunately, TypeScript's type system is also unsound. But it's unsound for a reason: it's a trade-off that its designers made to achieve compatibility with the existing JavaScript ecosystem. Now the learner knows that the problem is real and they're not imagining it; they know that the problem isn't their fault; and, if the lesson did its job, they know how to spot the problem and work around it.

This post was written by the Execute Program team. Execute Program teaches TypeScript, JavaScript, Regular Expressions, SQL, and more using thousands of interactive code examples. It has an integrated spaced repetition system to ensure that you don't forget what you've learned!