Inspired by a recent post by Bozhidar, I decided to write a little bit about code meaning in some contexts. And, the same as Bozhidar, I’ll start with a citation:

Programs must be written for people to read, and only incidentally for machines to execute.
– Harold Abelson, Structure and Interpretation of Computer Programs

What’s the best way “for people to read” a program? Maybe, the natural language. It’s not that easy to write a program the same way you would write in English, for example, but at least on software tests, I believe this worry should be even more present than in production code. And the reason is, tests drive how your application work (or how it should work) and how you can refactor things. I believe most people don’t really understand this point.

So, let’s suppose we want to write some code to test a monetary system, where we can sum money. It’s tempting to write something like:

(deftest sum-dolars
  (is (= {:usd/amount 2M}
         (money/plus {:usd/amount 1M} {:usd/amount 1M}))))

(deftest sum-different-moneys
  (is (= {:usd/amount 1M :uyu/amount 1M}
         (money/plus {:usd/amount 1M} {:uyu/amount 1M}))))

But I believe you should write your tests in English first…

summing only USD
  check the sum of 1 USD and 1 USD is 2 USD

summing different moneys
  check the sum of 1 USD and 1 UYU is a combination of 1USD and 1UYU

And then try to translate to code and “add parenthesis” when things make sense:

(deftest summing-money
  (testing "summing only USD"
    (check (money/sum (usd 1) (usd 1))
           => (usd 2)))

  (testing "summing different moneys"
    (check (money/sum (usd 1) (uyu 1))
           => (combination-of (usd 1) (uyu 1)))))

The linter

Now… this can pose a problem to linters. For example, clj-kondo gives us the following error: my_file.clj:4:12: error: unresolved symbol =>. And it’s not incorrect: really, the check macro uses some “arrows” to try to simulate an deprecated test library called Midje. That’s fine… until it isn’t. For example, I saw lots of people issuing declare forms because they were using Cursive, for example. These are complicated: your code seems to do something when in fact, it’s just to please a single tool / IDE / etc. For people to read is inverted: when a person reads that code, it’ll think that => for example comes from some declaration and is re-implemented somewhere else, but it’s simply not true at all. Your REPL will gladly “goto var definition” to an empty one.

Now, clj-kondo is a wonderful tool and allows you to customize the linter by defining a “custom macro expansion” in linter time. This is great, and I want to check it soon. The problem is not really with the linter, but with people that deliberately makes the code harder to read to make it easier for a machine to lint. I already saw people that don’t want to use a specific library/tech because it will make it harder for their editors/linters/formatters/etc to indent/lint/whatever the code correctly – even when all tests pass and the tech will make the code easier to understand. It’s like a modern version of “I will just use this library if VisualStudio have good support for it” that I experienced on my small C# experience…

There’s also a corollary: sometimes, editor/IDE features will make the code harder – code folding, auto-imports, etc. In my Clojure experience, I saw already namespaces with 80 :require lines, just because the IDE would gladly fold and organize imports for it. Also, namespaces like c-sk-fn because, again, the editor did support a way to auto-generate aliases. This is all terrible: some of these imports were used only once on the code, so your “goto file” becomes gigantic – lots of places to look just to understand a single code fragment…

Closing thoughts

I believe that we must have a code style. But I also believe that a code style is not an excuse to make your code harder to understand. There are a lot of code that I do that it’s not “lintable” nor “formattable” by default, for example, a macro to change time/now, the check macro that I showed earlier (and the “mocking libraries” that come with it), and much more. But it’s the difference between writing a test like this:

(cards/deftest clojure-simple-autocomplete
  (async done
    (async/go
     (client/disconnect! :clj-simple)
     (let [repl (clj/repl :clj-simple "localhost" 2233 identity)
           chan (async/promise-chan)]
       (clj/disable-limits! repl)

       (testing "completing core functions"
         (let [res (simple/for-clj repl 'repl-tooling.integration.fixture-app "prn")]
           (is (= [{:candidate "prn" :type :function}
                   {:candidate "prn-str" :type :function}]
                  (async/<! res)))))
       (done))))

Or this:

(cards/deftest clojure-simple-autocomplete
  (h/async-with-repl "Clojure simple autocomplete"
    (testing "completing core functions"
      (let [res (simple/for-clj repl 'user "prn")]
        (check (await! res) => [{:candidate "prn" :type :function}
                                {:candidate "prn-str" :type :function}])))))

There are lots of boilerplate code on the first version, is insecure (can keep a connection open if the test fails), can hang (if the channel does not resolve, it’ll hang forever) it’s harder to check for errors (the default matcher does not show diffs nor show where something didn’t match) and so on. Also, I’m pretty sure I can start a new test easily with the second version, instead of having to copy-paste code from other tests on the first version.

So, make the life easier for you and your friends! Make code that is easier for people to read. The computer will be able to execute it alright.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: