Desire paths in code

Have you noticed those bootleg paths that shortcut between two sidewalks? They're common in parks and on campuses. They're called desire paths. They show where the sidewalk should have been (sometimes the park or school will actually give in and pave them, leading to strange results when seen on a map).

Programming languages have desire paths, too, and I'd argue they demonstrate shortcomings in those languages. Indeed, people often claim that a language doesn't need a particular feature, but the desire paths in their code demonstrate otherwise. Thus the old saying, “Don't listen to what people say; watch what they do.”

I'll give two examples from Ruby and one from JavaScript.

NotImplementedError as a desire path for interfaces

Ruby's NotImplementedError is not a marker for an abstract method on a base class. It means “a feature is not implemented on the current platform."1

Hardcore duck-typers will tell you that it's unnecessary to enforce interfaces and that one of the beauties of Ruby is that you don't have to go around writing boilerplate to do so.

And yet, numerous Ruby codebase misuse NotImplementedError as a way to enforce interfaces. Don't listen to what people say; watch what they do. People want interfaces.

Symbols as a desire path for enums

Ruby symbols are everywhere in the language. Sometimes they are unbounded. For example, if we public_send a symbol in the context of dynamic method dispatch, we might not know the methods on an object until runtime—heck, they may not even exist until runtime, as they could be built from an impure source such as a database, timestamp, or random value.

But at other times, symbols are part of a known universe of values. In that situation, the Ruby way is to write tests to exercise all the variants. But because symbols sometimes have long names, and because spelling is not every coder's strong suit, it's easy to typo a symbol's name. Because of Ruby's groovy, anything-goes attitude, tooling support for typo-d symbols is difficult or impossible.

And so sometimes ad hoc double-checking occurs. If 14 enum values are defined, it can be hard to tell whether that 273-line test file (located in an entirely different part of the project) exercises every variant. So the case statement's default branch throws an error.

Sometimes, contrivances like this occur:

module Infielders
  Pitcher = "Pitcher"
  Catcher = "Catcher"
  FirstBaseman = "FirstBaseman"
  SecondBaseman = "SecondBaseman"
  ThirdBaseman = "ThirdBaseman"
  Shortstop = "Shortstop
end

Now we at least have tooling support.

And of course, Rails implements enums on models.

Ternary abuse as a desire path for conditional expressions or do expressions

In React JSX/TSX, this kind of code crops up:

<Foo>
  {something === 3 ? (
    somethingElse === 17 ? (
      <ThreeAnd17Thing number={Math.floor(foo * Math.PI)}>
        "We got that 3 and 17 situation this time"
      </ThreeAnd17Thing>
    ) : (
      "We got that 3 and not 17 situation this time"
    )
  ) : somethingElse === 17 ? (
    "We got that not 3 and yes 17 situation this time"
  ) : (
    <NeitherThreeNor17Thing
      bar={baz(moo + 22)}
      quux={xyzzy}
      corge={grault}
      thud={wubble}
      flob={garply}
    >
      "We got that not 3 and not 17 situation this time"
    </NeitherThreeNor17Thing>
  )}
</Foo>

The problem is that we need the code to be an expression inside the component, and we aren't quite ready to extract a subcomponent,2 so we have a hot potato where we have to keep the code as a long expression without any intermediate statements.

JavaScript lacks the ergonomics common to expression-based languages. For example, in Clojure, you'd do something like this:3

(if (= 3 something)
  (if (= 17 somethingElse)
    (let [fooTimesPi (* foo Math/PI)
          number (Math/floor fooTimesPi)]
      (ThreeAnd17Thing
        {:number number}
        "We got that 3 and 17 situation this time"))
    "We got that 3 and not 17 situation this time")
  (if (= 17 somethingElse)
    "We got that not 3 and yes 17 situation this time"
    (let [bazArg (+ 22 moo)
          bar (baz bazArg)
          args {:bar bar
                :quux xyzzy
                :corge grault
                :thud wubble
                :flob garply}]
      (NeitherThreeNor17Thing
        args
        "We got that not 3 and not 17 situation this time"))))

Note that we can let-bind locals within the expressions, so we can set the hot potato down, as it were—while the whole form remains an expression.

In JavaScript, by contrast, the keywords if and else introduce statements, not expressions, and we don't have a way of binding locals in an expression, so we have to nest function calls and abuse ternaries.

What's missing is do expressions, which is a Stage 1 TC39 proposal.

<Foo>
  do {
    if (something === 3) {
      if (somethingElse === 17) {
        const fooTimesPi = foo * Math.PI;
        const number = Math.floor(fooTimesPi);

        <ThreeAnd17Thing number={number}>
          "We got that 3 and 17 situation this time"
        </ThreeAnd17Thing>
      } else {
        "We got that 3 and not 17 situation this time"
      }
    } else {
      if (somethingElse === 17) {
        "We got that not 3 and yes 17 situation this time"
      } else {
        const bazArg = moo + 22;
        const bar = baz(bazArg);
        const args = {
          bar,
          quux: xyzzy,
          corge: grault,
          thud: wubble,
          flob: garply
        };

        <NeitherThreeNor17Thing {...args}>
          "We got that not 3 and not 17 situation this time"
        </NeitherThreeNor17Thing>
      }
    }
  }
}
</Foo>

  1. Class: NotImplementedError, https://ruby-doc.org/core-2.7.1/NotImplementedError.html (last visited July 28, 2020) (emphasis added). ↩︎

  2. We should do that to get rid of the nested ifs and clean things up. The discussion that follows assumes we're not going to do that. ↩︎

  3. There's no React here; I'm pretending the components in the JSX code are function calls here. Also, please forgive errors: I don't have a Clojure environment set up to lint/indent this, so I did it by hand. ↩︎