Some Things Have To Be Ugly
By Artyom Bologov
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 ...))
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 ...)))
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 ...)))
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 ...)))
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.