This blog post takes you behind the scenes of my latest book, “JavaScript for impatient programmers” (which I’ll occasionally abbreviate as “Impatient JS”). It describes:
My idea for “Impatient JS” was: What would a book look like that teaches current JavaScript to programmers, as efficiently as possible?
In this section, I explain how I picked and presented the content.
Focusing on current JavaScript has one big advantage: You can tell a simpler, more consistent story, because recent features have eliminated many quirks and simplified many operations. The book does not explain old features that were superseded by newer and better features, but it includes references that cover them.
It is also satisfying that we can finally explore the full range of asynchronous programming in JavaScript: from callbacks to Promises to async functions to async iteration.
To keep the right balance between “too terse” and “too verbose”, I only mention the history of a feature when it helps understand that feature.
The following subsections describe techniques I’ve used for teaching topics.
If possible, readers should not just passively receive information, they should also be able to practice what they have learned. Two convenient formats for that are exercises and quizzes.
Two common ways of writing books about languages are:
“Impatient JS” combines elements of both. For example:
It covers the highlights of the standard library in a tutorial fashion, complemented by an exhaustive quick reference of the remaining functionality. The idea is that for much of the standard library, it is enough to know what’s there and to look up the details on demand.
The language is introduced step by step, so that chapters can be read consecutively (like a tutorial). But the book also keeps everything related to a given topic in one place (like a reference), via a trick: Some sections and chapters are marked as “advanced”. Readers can initially skip those sections and can become productive as quickly as possible. Once they are more familiar with the language, they can proceed to the advanced content.
One technique I like is to explain everything twice: First, state something (somewhat abstractly) in English, then prove it via code. For example: If you state “the multiplication operator always coerces its operands to numbers”, you can follow up with a REPL interaction:
> '3' * '4'
12
The redundancy helps readers: Exploring different aspects deepens the understanding. And by using different modes of explanation, you increase the chance of reaching your readers.
As important as practicing in front of a computer is, it should also be possible to read a computer book without a computer. To that effect, I attempt to guess what readers would want to try out and try it out for them.
For example, if you write “reading an unknown property produces the value undefined
”, then readers are probably curious how that works in practice. If you show them, they don’t need a computer to try it out. One way of doing so is via a REPL interaction:
> const obj = {};
> obj.unknownProp
undefined
This principle is related to the previous principle (explaining everything twice). But its goal is different.
These are techniques that I’ve found useful:
Sections: help partition text logically. It’s important to keep a balance between sections that are too small and sections that are too large.
Bullet lists: are easy to scan and great for enumerating items. If the items in a bullet list become too large, you can turn the bullet list into a section and the items into subsections.
Tables: can be considered two-dimensional bullet lists. They present information even more compactly. They are especially well suited for summarizing traits of things. The table at the end of this subsection is taken from the chapter “Callable values”.
Type notations: are yet another way of describing something. For example, you may write “func
is a function with a single parameter, a string, that returns a number”. Then you can follow it up with:
func(str: string): number
Once again, we are explaining the same thing twice: once in English and once in a formal notation. I’m using TypeScript’s type notation, which I explain in a chapter at the end of the book. A recent blog post of mine is an older version of that chapter.
Table: Capabilities of the four kinds of functions.
Ordinary function | Arrow function | Method | Class | |
---|---|---|---|---|
Function call | ✔ |
✔ |
✔ |
✘ |
Method call | ✔ |
lexical this |
✔ |
✘ |
Constructor call | ✔ |
✘ |
✘ |
✔ |
Some information is easier to convey visually, especially if structure or processes are involved. For example, the diagram below is from a chapter at the end of the book, that gives an overview of web development in general. The diagram depicts a typical webpack workflow.
Several artifacts are used for delivering the content (the book, the exercises and the quizzes). Let’s look at what those artifacts are and how they are produced.
The following diagram gives an overview. Quizzify and Exbuild are tools I wrote for myself (I’m currently considering if and how I could open-source them). Pandoc is an open-source document converter.
The book is written as Markdown, with one Markdown file per chapter. The output formats are:
All of the book artifacts are created via Pandoc, combined with filters (plug-ins) written in the programming language Lua. The website is created by a Node.js script that splits and post-processes Pandoc’s one-file HTML output.
For images, I distinguish:
Pandoc parses its input into an abstract syntax tree. Filters operate on that syntax tree and can transform it. In addition to structured text, the abstract syntax tree can also contain “raw” HTML and LaTeX that is passed on to the next output stage without any changes.
The Lua filters that I wrote often have two modes:
As an example, consider the following input Markdown:
::: tip
## Try taking a break!
For example, go for a walk or do a breathing exercise.
:::
Pandoc does not have built-in support for boxes with content. The idea is to use a Pandoc “fenced div” with a pre-defined CSS class to express a box. In the previous example, :::
plus the pre-defined class tip
starts the box; the three colons at the end, finish the box. A filter converts this div into an actual box.
This is the output in HTML mode, after the input was processed by the filter:
<div class="notebox">
![](img/icons/lightbulb-regular){height="24px"} **Try taking a break!**
For example, go for a walk or do a breathing exercise.
</div>
The filter did the following things:
tip
was only used to select the image for the icon. The actual CSS class for boxes is notebox
.For LaTeX, the filter embeds LaTeX. This time, the syntax for embedding is more verbose:
```{=latex}
\begin{mdframed}[style=exampledefault]
```
![](img/icons/lightbulb-regular){height="24px"} **Try taking a break!**
For example, go for a walk or do a breathing exercise.
```{=latex}
\end{mdframed}
```
Each quiz is defined via a Markdown file. A question looks like this:
## Spelling
1. Javascript
2. JavaScript
3. Java Script
Solution: 2
Quizzify converts the Markdown to a client-side, React-based program. You can check out the result online.
Exercises have two formats that I call “test” and “exercise”:
The following code is an (abridged) “exercise”:
/* npm t exercises/operators/typeof_exrc.js
Instructions:
- Run this test (it fails).
- Change the second parameter of each assert.equal() so that the test passes
*/
import {strict as assert} from 'assert';
test('typeof', () => {
assert.equal(typeof null, '???');
assert.equal(typeof undefined, '???');
// (Several assertions are omitted)
});
When it comes to exercises, the file structures for authoring and deployment are different. The tool Exbuild transforms the former to the latter when creating the ZIP file with the code. Why are the structures different?
Therefore, for a “test”, the test is deployed in the exercises/
directory, the tested file is deployed in the solutions/
directory. Optionally, there may also be a _tmpl
file with a skeleton of the solution; to be completed by readers.
Input (authoring) | Output (deployment) |
---|---|
exercises/foo_test.js |
exercises/foo_test.js |
exercises/foo_tmpl.js |
exercises/foo.js |
exercises/foo.js |
solutions/foo.js |
An “exercise” always involve a _tmpl
file with mistakes that readers have to fix or blanks that readers have to fill in. The non-template file contains the solution that readers should end up with.
Input (authoring) | Output (deployment) |
---|---|
exercises/bar_exrc_tmpl.js |
exercises/bar_exrc.js |
exercises/bar_exrc.js |
solutions/bar_exrc.js |
Not only are there many different artifacts – each one of them has to be created in two versions:
This is handled by Pantools, Quizzify and Exbuild via builds (Pantools is the Node.js app that manages Pandoc). Each build has a name and specifies:
When you create an artifact, you specify which build to use.
All three kinds of content contain JavaScript code:
The best way of avoiding typos in code is to run it in unit tests.
For the book, I wrote my own tool, Marktest, that extracts Markdown code blocks and turns them into mocha unit tests.
Take, for example, the following Markdown content:
In the following code, the value returned by the `.catch()` callback in line A becomes a fulfillment value:
<!--marktest before:
const retrieveFileName = () => Promise.resolve();
-->
```js
retrieveFileName()
.catch(() => {
// Something went wrong, use a default value
return 'Untitled.txt'; // (A)
})
.then(fileName => {
// ···
});
```
The comment tells Marktest to put the provided line of source code before the text of the code block. Text in half-brackets (such as return
) is not shown in the ebooks and only added to the unit test. Thanks to the return
, the test created by Marktest is a proper asynchronous test (because it returns a Promise):
test('Line 238, js', () => {
const retrieveFileName = () => Promise.resolve();
return retrieveFileName()
.catch(() => {
// Something went wrong, use a default value
return 'Untitled.txt'; // (A)
})
.then(fileName => {
// ···
});
});
The line number mentioned in the test name helps with debugging when there are errors. I’ve indented the output to make it easier to read. Marktest currently does not indent code, to avoid breaking it.
REPL interactions are also supported by Marktest. For example:
This is a REPL interaction:
```node-repl
> 3 + 5
8
```
REPL interactions are converted into a series of invocations of assert.deepEqual()
:
test('Line 70, repl', () => {
assert.deepEqual(
3 + 5
,
8
);
});
Marktest has more features. For example:
To understand how unit-testing a quiz works, let’s use the following question (written in Markdown) as an example:
```js
function foo(x=true, y) {
return [x, y];
}
const result = foo();
```
What happens?
1. `assert.deepEqual(result, ['a', 'b'])`
2. `assert.deepEqual(result, ['a', undefined])`
3. `assert.deepEqual(result, [true, 'a'])`
4. `assert.deepEqual(result, [true, undefined])`
Solution: 4
Unit-testing the correct answer 4 is straightforward – we simply wrap both code an assertion in a test()
:
test("Parameter default values #4", () => {
function foo(x=true, y) {
return [x, y];
}
const result = foo();
assert.deepEqual(result, [true, undefined])
});
For incorrect answers, we cannot simply switch from assert.deepEqual()
to assert.notDeepEqual()
, because the answer may also be incorrect due to the code throwing an exception. Therefore, we wrap the whole body of the test()
in an assert.throws()
– which means that we expect the wrapped code to throw an exception. That exception can come from either the code or the failure of assert.deepEqual()
.
test("Parameter default values #1", () => {
assert.throws(() => {
function foo(x=true, y) {
return [x, y];
}
const result = foo();
assert.deepEqual(result, ['a', 'b'])
});
});
Unit-testing the exercises is relatively simple because the file structure which is authored is the same as one where a reader has successfully completed all exercises. Therefore, we just need to run all tests (i.e., all “tests” and all “exercises”).
Making sure that the code in the book and the quizzes is testable is more work. That raises the question: is it worth that work?
On the minus side, seeing assert.equal()
etc. in the code takes some getting used to. Compare:
// Old style:
console.log(3 + 4); // 7
// New style:
assert.equal(3 + 4, 7);
On the plus side:
assert.throws()
is quite powerful in this regard.My tools also perform a few other checks, such as:
npm t
command mentioned at the beginning of an exercise correct?I’m interested in feedback: