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.
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
But what if
But okay, now we need to make some conditional logic in the Patient/Person branch.
Humans are never easy to encode...
Can I inline it in the toplevel
Now to the fun stuff: some entities might have sub-entities.
These need encoding too.
So we also need to
At this point, one (Artyom) might be tempted to rewrite it all with CD(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.
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.
;; Four lines
(case (:type data)
("Patient" "Person") (process-person ...)
"Address" (process-address ...)
(process-default ...))
;; Six lines
(let [type (:type data)]
(cond
(nil? data) (throw ...)
(contains? #{"Patient" "Person"} type) (process-person ...)
(= type "Address") (process-address ...)
:else (process-default ...)))
;; Thirteen lines
(let [type (:type data)]
(cond
(nil? data)
(throw ...)
(contains? #{"Patient" "Person"} type)
(cond
(...) ...
(...) ...
:else ...)
(= type "Address")
(process-address ...)
:else
(process-default ...)))
;; 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 ...)))
Deliberately Ugly