openlang

Translating old CoffeeScript by hand is a decent JavaScript lesson

An unfashionable exercise that works better than it has any right to. Give a learner a small old CoffeeScript file and have them rewrite it as modern JavaScript without running the compiler.

Nearly every piece of sugar has a modern counterpart, and that's the point, because the counterparts are close and not identical. @name is this.name. == always meant ===. a ? b looks like a ?? b and very nearly is (except that CoffeeScript's version also accepts a variable that was never declared, compiling to a typeof check, while ?? on an undeclared name throws a ReferenceError, which is precisely the kind of thing a learner only finds by getting it wrong once). Ranges, for own, implicit returns and the soak accessors all have the same flavour of almost.

The learner ends up reading the JavaScript spec for semantics instead of syntax, and I can't think of many beginner exercises that manage that.

The conversation continues 11

Follow the reasoning
  1. Permalink

    The mistake I'd expect most is || in place of ??, because it's the older habit and it passes a quick look. They differ on every falsy value that isn't null or undefined.

    const saved = 0
    console.log(saved || 10, saved ?? 10)
    console.log('' || 'none', '' ?? 'none')

    That prints 10 0 and then none followed by an empty string. CoffeeScript's ? treats 0 and '' as present, the same as ??. So a translation to || changes behaviour on exactly the values a settings screen tends to hold, and annotating saved as number won't flag it, since 0 is a perfectly good number.

  2. Permalink

    And then the value comes from an input. I clear the quantity field and hit save. input.value is '', not null, so ?? happily keeps the empty string and the screen shows a blank where the default should be, while || puts 10 back (and would also put 10 back over a real zero someone typed on purpose). Neither of them knows what an empty field means. Somebody has to decide that in the code that reads the field, before either one gets a say.

  3. Permalink

    I've always liked the word CoffeeScript picked for ?, existential, a little more than I should. Said aloud it sounds like it asks whether the thing exists at all, and for an undeclared variable that's literally what the compiled typeof check asks. For a declared one it only asks whether the value is null or undefined, which is a much smaller question wearing the same big word. JavaScript went with nullish coalescing, a clumsier name that at least says which values it means.

  4. Permalink

    The undeclared case mattered (each file was wrapped in a function, so if jQuery? was how you asked whether the library had loaded). Translated to jQuery ?? fallback, it throws on any page without jQuery.

  5. Permalink

    The soak call is the one I'd put first in that exercise. CoffeeScript's callback?() compiles to a typeof callback === "function" test before the call, so a callback that holds a string or a number is quietly skipped. JavaScript's callback?.() checks only for null and undefined. Hand it a string and you get a TypeError, because a string isn't callable, even though the two lines differ by a single dot.

  6. Permalink

    TypeScript catches the declared version, since callback?.() on a string | (() => void) | undefined is a compile error. A value from JSON that only claims to be a function still needs typeof callback === 'function'.

  7. Permalink

    I'd make it a bit gentler. Let coffee -c produce the first draft, have the learner explain every line of the output, and then clean it up into the JavaScript they'd actually write. They still meet the typeof checks and the collected arrays, just with the answer key already on the page. Pure hand translation sounds good, but a learner who guesses wrong about ? has nothing telling them so until something breaks much later.

  8. Permalink

    The answer key is the problem. Compiled CoffeeScript is CoffeeScript's idea of JavaScript (temporaries named ref and j, a hasProp helper for every for own, a ternary that checks direction on every range loop, all of it perfectly correct and none of it written the way a person writes), and a learner explaining it line by line mostly learns how the compiler thinks. Interesting, but not the lesson. Guessing wrong about ? is the useful part. Pair the exercise with a small test file instead, so the wrong guess shows up as a failing case right away.

  9. Permalink

    Ranges are where I'd expect the surprise. The cart page renders a row per item with for i in [1..count]. I empty the cart, count is 0, and the page shows rows 1 and 0. CoffeeScript ranges count down when the end is smaller than the start, so [1..0] has two elements, not none. The hand translation would be for (let i = 1; i <= count; i++), which does nothing at zero, so the learner fixes a bug without ever noticing it was there (which is either the best or the worst outcome of the exercise, I honestly can't decide).

  10. Permalink

    That one has a documented way out, by 1, which fixes the direction (so for i in [1..count] by 1 runs zero times at zero), and it was easy to forget because the downward count only shows up on the one input nobody tries first. The silent fix is the bit I'd want the learner to say out loud, though. If their translation behaves differently from the original, they should be able to name the input that shows it, and count = 0 is a fine one to name.

  11. Permalink

    Siding with corwin on this one, at least for loops. The compiled range loop is the worst place to start explaining anything. The direction check sits in the condition as 1 <= ref ? j <= ref : j >= ref and then again in the increment, so a learner walking through it line by line is mostly learning to read nested ternaries, and the actual question of what happens at zero gets buried under that.

Other sides of the same question

  1. round(2.5) gives 2, and learners don't believe it

    Learners keep guessing that round(2.5) is 3, and both languages I reach for say 2. Python gives the integer 2, Julia gives 2.…

  2. 3 + 4 * 2 is 14, and that is the easy part

    Everyone new to Smalltalk meets 3 + 4 * 2 early and gets told it's 14, because binary messages go left to right with no arith…

  3. (x, x) does not mean both halves are equal

    Learners keep reading OCaml's (x, x) as "both halves are equal". It isn't that, and it isn't even legal. A newcomer tends to…

    Replies 5ocaml