Some Things Have To Be Ugly

By Artyom Bologov A jarring beige thumbnail with

Some things are just ugly and you have to live with it. Or even make some things ugly deliberately—to highlight the complexity behind the code.

Accidentally Ugly

I was working on "humanization" of medical data on my dayjob. The task is pretty simple: take the data, check its type, output the human-readable text. The data is strongly typed too, so it's a matter of dispatching over the type. I'm gonna use case!

;; Four lines
(case (:type data)
 ("Patient" "Person") (process-person ...)
 "Address"            (process-address ...)
 (process-default ...))
Simple case over data types

But what if data is malformed or null? We need to handle that too! But case no longer cuts it, because we need to make arbitrary checks. Well, we can encode these in the "else" branch of the case, but it's going to get dirty and deep indentation-wise. Better use good old cond:

;; Six lines
(let [type (:type data)]
  (cond
    (nil? data)         (throw ...)
    (contains? #{"Patient" "Person"} type) (process-person ...)
    (= type "Address")  (process-address ...)
    :else               (process-default ...)))
cond dispatch

But okay, now we need to make some conditional logic in the Patient/Person branch. Humans are never easy to encode...

;; Thirteen lines
(let [type (:type data)]
  (cond
    (nil? data)
    (throw ...)
    (contains? #{"Patient" "Person"} type)
    (cond
      (...) ...
      (...) ...
      :else ...)
    (= type "Address")
    (process-address ...)
    :else
    (process-default ...)))
cond dispatch

Can I inline it in the toplevel cond? Yes, but what if there are five "special" cases like that? The cond grows longer, more branched and duplicated.

Now to the fun stuff: some entities might have sub-entities. These need encoding too. So we also need to recur on the function containing these:

;; Sixteen lines
(let [type (:type data)]
  (cond
    (nil? data)
    (throw ...)
    (contains? #{"Patient" "Person"} type)
    (cond
      (...) ...
      (...) ...
      :else ...)
    (= type "Address")
    (process-address ...)
    (= type "Entity")
    (str ...
         (recur (:sub-entity data) "SubEntity"))
    :else
    (process-default ...)))
Cond+recur, the control flow gets convoluted

At this point, one (Artyom) might be tempted to rewrite it all with defmethod-s. Because we're dispatching over data and then delegating some logic to specialized methods. Must be a beautiful architecture... But it'll take twice as many lines and won't be able to handle more complex checks. Not today, methods.

...

You might see where I'm getting with this: Some code is ugly enough to not fit in. No amount of cool language constructs can handle real world. Some home-made macros can manage it... for a while.

But some things are inherently and inevitably ugly. Because problem domain often is ugly enough to require ugly code. Face it.

Deliberately Ugly

Now to the controversial part: you should make your code ugly. No, not all of it, but some parts at least!

Ugly code is the code that embraces the complexity. Complexity of the data, complexity of the algorithm, complexity of the problem domain. Making ugly code in a real world setting is not shameful. It's the only mode of existence we have when we encounter complexity.

Complexity needs a lot of verbal and mental energy to process and convey. We (programmers) are more fluent in code than in words. Ugly code is a better representation of complexity than some domain glossary. It's a literal recipe for what complexity there is and how to handle it.

It must stay that ugly. And it must be made ugly if it not yet is. Embrace ugly code and fight the complexity on your own terms.

Leave feedback! (via email)