1. 10

  2. 8

    Normally not a huge fan of Eric Elliot, but this article was pretty good. One issue I want to raise is his quote about this code block:

    const g = n => n + 1;
    const f = n => n * 2;
    // Imperative composition
    const doStuffBadly = x => {
      const afterG = g(x);
      const afterF = f(afterG);
      return afterF;
    // Declarative composition
    const doStuffBetter = pipe(g, f);

    The imperative style version requires logic that should be tested … Assuming f and g have their own unit tests, and pipe() has its own unit tests (use flow() from Lodash or pipe() from Ramda, and it will), there’s no new logic here to unit test [in the declarative style].

    First of all, the “imperative composition” here is also “declarative”. It’s equivalent to doing

    doStuffBadly x = 
      let afterG = g x
          afterF = f afterG
      in afterF

    In Haskell. I’d argue it’s less coupled than the declarative version in javascript, because you’re not adding a dependency on your piping library.

    Second, you still need to test doStuffBetter. While it’s just a composition of f and g, that doesn’t change the fact you are composing f and g to do something, and you have to confirm that composition is the one you actually want. For example, in Python:

    def first(l):
      return l[0]
    def my_max(l):
      return first(sorted(l))

    my_max is just the composition of two pure functions, first and sorted. But even if I have unit tests on those two, my_max is still broken: it actually returns the minimum.

    1. 2

      I like the post because there are many examples, but I think there is a simpler way to frame it. In the days of goto-littered code, the problem was that control flow could jump all over the place in a function, making it hard to understand what is going on within it during its execution. Side-effecting code can do the same thing, especially when called functions send data out of the current function (rather than returning it) or get new data from outside of it. To the degree that this is considered normal, we want to tap various parts of the function to understand what is happening, so the question becomes how do we write code where we don’t have to do that (much). The tradeoff is that when components don’t manage their communication with other components, the “composer” does and it bears the brunt of dependency (and complexity).

      1. 1

        I wonder if the author would hold the position of mocking as a code smell across all languages or only to the languages covered in the article.