Beautiful Apps, Ugly Code
prog21.dadgum.com
prog21.dadgum.com
It also complains about short examples. The reason these examples tend to be small is because they're examples. If somebody posted a couple thousand lines of Haskell, nobody except Haskell programmers (and not even most of those) would read it, however elegant it may be. That doesn't mean longer elegant programs don't exist!
Now, it is, of course, true that inelegant code can lead to a beautiful product. It would be silly to claim otherwise. But that is not optimal; just because it works, does not mean it couldn't be better.
Next the article claims that there is a "curious lack of concrete examples" of purely functional programs. This is patently untrue: there are plenty of great Haskell programs about on the internet. The canonical example is XMonad, which is particularly apt because it is short enough to read (< 3000 lines).
In the end he presents a false dichotomy: you can either have elegant code or focus on the product. Of course, the two are not mutually exclusive at all: writing elegant code can help you produce a more robust program that is easier to maintain.
He probably means USEFUL examples, of what one is likely to build. No one will write an XMonad.
And XMonad is not a product -- something users want and that solves a business need. It's a language's innards, a developer tool at the most basic.
Still, the example is an obscure Window Manager? And a tiling one, at that? Not the premier example of what he asks for, end user successful complicated software that has had to mature and deal with lots of real world corner cases, bugs etc.
I completely disagree. Beauty scales best. It's almost always deadlines, and not complexity, that uglify code. In a culture that values business value over code extensibility and reason-ability (my word), it is very difficult to keep a codebase clean and elegant (but it's possible). Objects make that 100x harder.
I see everything in the world as a collection of objects and their relationships. That may not be the only way of seeing things and may not always be the best way of seeing things, but it is an approach my mind naturally takes to understanding a system.
I'm down with OOP.
By rigorously separating the concerns into a clean object model, LMAX achieved a level of performance which might correctly be labeled "miraculous". http://screencast.com/t/g67kFj8nRue
It's written in pure Java. Amazingly, I haven't found anything that's achieved better performance thus far. It's very elegant.
They're writing really tightly optimized C. In Java.
Here are some of the many ways to silence someone who is shouting in all caps:
(string-downcase yelling) ; common lisp
strtolower($yelling) // php
yelling.toLowerCase() // js
lc($yelling) # perl
yelling.lower() # python
I don't really care about the parens and where they are, but the fact that clisp, php, and perl put this function in the global namespace bugs me to no end. It's a function which only makes sense when you have a string. Other things don't have the sort of "case" such that you could "lowercase" them.The only problem with list.sort() in Python is that it's void; it returns None. It should either return self (and sort self) or return a sorted copy of self, like sorted() will.
On the other hand, in JavaScript, array.sort() is known to be a little broken:
> [48, 19, 7, 14, 30, 22, 45].sort()
[14, 19, 22, 30, 45, 48, 7]
Wat.It's true that I have ultimate responsibility for my code, and it's true that in Python I can write:
x = 3
x.lower()
...and unlike Java it will not complain that it has no idea how to "lower" 3. But it still suggests like Java suggests that we're going to narrow down the wildly branching tree of possibilities.When I'm sitting in the Python REPL, and I have an object x, I'll often just call dir(x), to see what I can do with it. What would Common Lisp tell me? It would kindly tell me that it's syntactically valid to call (string-downcase 3), even though that will produce a noisy error. So if I wanted to list all of the things I can do with 3, we would be here for a long time.
AttributeError is a complaint, isn't it?
The point I'm making is I suppose one about personal mental hygiene, but I am choosing an odd language for it. Because I am talking about how computers can reason for us, and can help us reason. Python will not complain about x.lower() until it actually occurs; it doesn't reason at all about x. On the other hand, the Python interpreter gives you some leeway to sit inside the code as it's being run, to say "okay, what can I do with x now that I have it?" -- which is a start.
It's especially pivotal for me when I'm working with an external software package which isn't well-documented. I hate working with someone else's packages in Lisp. With Python, I can usually reverse-engineer what they were thinking pretty easily. With Java, your code editor provides another method to do the same thing, preventing you from writing things which wouldn't have made sense to the package designers -- this helps you know what does make sense, to help you understand what they were thinking.
Like I said, my broader point is about mental hygiene, and there is a missing connection between mental hygiene and what computers can do for me. I think this is best summarized by Minsky and Sussman's reminder: "programming is a good medium for expressing poorly understood and sloppily formulated ideas." That's the missing connection. When I have a function which I don't completely understand, assigning it to a namespace helps me to be formal and precise about the situations where it is useful, and allows me to reason about the situations where it is not useful.
So for all that it might pride me that I can just look up the array API on developer.mozilla.org and quickly learn the difference between "shift", "splice", and "unshift", that's not as important to me. It's something more poignant that interests me, something about how, when I have a string, my brain quickly reasons about what functions are correct for it. I don't know how much Hungarian notation you've worked with, but the property-based notation has some Hungarian quirk to it where you can just feel that you've got the right sort of expression.
The same level of fail will accumulate regardless of the language if you don't know what you are doing.
Page "xv" of the book "A course in mathematics for students of physics" (volume 2) by Paul Bamberg and Shlomo Sternberg contains a table (do a "look inside" on Amazon) that shows four forms of Maxwell's equations as they evolved over the course of history. Each change expresses what the previous one did, but contributes something more that wasn't possible or was very difficult to express with the earlier form.
The first form explicitly uses cartesian coordinates. The second form is the vector notation, which helps conceptualize and work with the equations in any coordinate system appropriate to the problem (circular, cylindrical, etc.). The third is the relativistic "four-vector" formulation, which, though it says the same thing as the vector form, allows extension to curved spacetime. The fourth exterior calculus formulation (which just reads "dF = 0, and 𝛿F = J"!) covers all of the preceding ones, and also takes exactly the same form when applied to circuits (the kirchoff laws, etc.) which is not something you can say for the preceding forms, while allowing generalization to higher dimensions. The mathematical machinery invented for that simple looking expression is incredibly beautiful (and that book does a great job of its exposition, imo).
This kind of beauty in software is very desirable in my opinion and we do have examples like MapReduce to show for it. Yes the machinery to do a distributed MapReduce is complex, but the concept is simple and flexible and has a bit of the "exterior calculus" fragrance to it. Another is continuations in Scheme - which not only can replace any "mundane" control transfer (break, continue, throw, catch, and ilk) but can do much more than just those and lets you roll your own too.
I believe that is the kind of beauty we should be striving for in software, not just the "pretty code" kind.
(minor edits)
Having worked with extremely large code bases, I can say that the practice of re-factoring goes a long way. The initial time investment seems excessive, but it pays significant long-term dividends when it comes to maintenance. Not only is it easier to extend the solution but also fix bugs and supply patches. Furthermore, clean code (not just syntactically) is easier to understand by others.
Writing clean code is possible, it just takes a bit of discipline to go back and re-write. I've only worked with a handful of people who did this, but their code was the most elegant I've seen, easy to work with, and widely applicable.
There is a false dichotomy at play here I think - it's possible to write good code and also provide business value, one does not imply the abandonment of the other.
Trying to write clean, elegant code does not mean you then get to throw business concerns out of the window, nor do business concerns mean you get to ignore code quality.
To me, quality code is as clean + maintainable as possible while maintaining simplicity. Some of the more 'elegant' approaches (the 1-line incorrect haskell quicksort springs to mind) seem to somewhat sacrifice simplicity for the power of a clever trick. I'd prefer a more verbose + arguably simpler solution to that.
The big mistake people make here is to act as if the code doesn't have an impact on the business value, of course it does. It affects the quality of the product in 1000 different often less-than-obvious ways. To begin with, you have maintainability and extensibility which literally have extremely direct affects on your ability to sustain and grow your product, but you also have endless other factors which are a direct consequence of code quality.
For example, if your codebase starts getting really crufty and closely-coupled, it starts to become very hard to write code that doesn't fit into the design as-is, so if the customer/business need you to do X, X might turn out to be insanely hard, completely unintuitively to the business people. Try explaining to them how that's related to the code.
Also, try choosing naive O(n^2) or even O(n!) algorithms when you are scaling some software up to heavy levels of usage. The abstract non-business minded concerns start mattering quite a bit to the business there.
The article implicitly assumes that pure functional programming contributes positively to elegance, and since it doesn't define "elegance", it's hard to argue with it. I don't think however there is a strong consensus in the programming community that pure functional programming contributes positively to "elegance" understood as defined above, and this is to me the fundamental flaw of this article.
But assuming that ugliness is still inevitable, I'd still rather work in a language/environment/culture where ugliness can be kept at bay as long as possible. If ugliness is simply a property of long programs, then obviously languages that let you write shorter programs should scale beauty better.
Adding to the small examples, I dipped my toe into Android development tonight while at the same time getting Clojure to run on it. Clojure let me do:
(defmulti handle-key (fn [key] [key]))
(let [f #(eval (symbol (str "KeyEvent/KEYCODE_" %)))]
(doseq [[k letter] (map list (range 10) (seq "PQWERTYUIO"))]
(defmethod handle-key [(f k)] [key] (st k))
(defmethod handle-key [(f letter)] [key] (st k))))
(defmethod handle-key :default [key] false) ; don't handle event
(Which may not even be the most idiomatic Clojure-way since I'm still reasonably new to that as well.)And the first-thought for how I would do that in Java:
private boolean handle_key(int key) {
switch(key) {
case KeyEvent.KEYCODE_0:
case KeyEvent.KEYCODE_P: return st(key); break;
case KeyEvent.KEYCODE_1:
case KeyEvent.KEYCODE_Q: return st(key); break;
case KeyEvent.KEYCODE_2:
case KeyEvent.KEYCODE_W: return st(key); break;
case KeyEvent.KEYCODE_3:
case KeyEvent.KEYCODE_E: return st(key); break;
case KeyEvent.KEYCODE_4:
case KeyEvent.KEYCODE_R: return st(key); break;
case KeyEvent.KEYCODE_5:
case KeyEvent.KEYCODE_T: return st(key); break;
case KeyEvent.KEYCODE_6:
case KeyEvent.KEYCODE_Y: return st(key); break;
case KeyEvent.KEYCODE_7:
case KeyEvent.KEYCODE_U: return st(key); break;
case KeyEvent.KEYCODE_8:
case KeyEvent.KEYCODE_I: return st(key); break;
case KeyEvent.KEYCODE_9:
case KeyEvent.KEYCODE_O: return st(key); break;
default: return false; break;
}
}
It's not immediately clear to me how I could make that Java code shorter, and it's not exactly very pretty. (Long switch statements rarely are in my opinion.) (After some thought, I suppose you could use Java's reflection capabilities.)Anyway, it's a while since I've done any Java programming, so I might have my regex syntax incorrect, but:
private boolean handle_key(int key) {
if(KeyEvent.keyCodeToString(key).matches("KEYCODE_[0-9QWERTYUIOP]") {
return st(key);
}
return false;
}
is a lot shorter and easier to read than the Clojure code. It's not as efficient as the Java code you have, of course, but I suspect the Clojure code ends up rather inefficient too with all the taking apart and gluing together of symbol names.Clojure provides a constant time dispatch function called case that is perfect for this.
If it is designed to expect [rigid design] certain set of rules and not accomodate new and changing requirements, or if it is designed to accomodate unknown/made up requirements [overengineered design], you can endup with code that looks either like a framework for frameworks' sake or a perfect code for the set of known requirements.
In both cases, you endup either with code that cannot easily be modified [rigid] or you endup taking many shortcuts because the original design did (and could) not anticipate new requirements [overenginnered].
One of the ways to circumvent it is to know what you need design and what you need to be able to extend without modifying the original intent.
Take an iPhone app for example: The simple, elegant way to implement a page change in the Mail app would be a to just switch the page like this:
currentPage = pages[nextIndex];
However, when you look at the actual app, you see that it's not just a simple flip. The previous page flies out to the left, the next page flies in, there are at least 5 different tweenings happening to move the navigation buttons, page title, etc.The resulting code is probably far from what you would call simple and elegant.
Pick any program outside of the homework range, any
program of 200+ lines, and it's not going to meet a
standard of elegance.
That's because, as the article says, smallness is usually presented as an integral part of elegant code. 200+ lines of code is not small, by definition. 200+ lines of code also look like a jumble in any language (especially when you don't know the language well, but even when you know the language very well), unless you take the time to pick it apart and understand it.I think the main problem is this: you can't judge elegance from merely glancing over code. You have to understand it to judge whether it is beautiful. Unfortunately, because all those small examples have primed us to equate smallness with beauty, any larger program is swiftly deemed ugly. This takes conscious effort to overcome: large programs can be beautiful, but they require you to discover the beauty. It won't be obvious.
https://github.com/drostie/ducky/
Now, it's 277 SLOC by GitHub's reckoning, and it solves a really esoteric problem (providing a language to wrap asynchronous calls in, so that you can write asynchronous code synchronously). The problem wouldn't even be particularly clear if you didn't know something about Node.js and how the API is set up. In that sense maybe you're right.
But there's another sense in which you're maybe not right. Perhaps this is just me feeling too much fatherly affection for my code, but I like to think that even if you were baffled by the expected use of the project, you could still break promises.js into chunks (in fact, in the source it's done for you) which helps you understand what each of these methods is, and what it does, and how to use it to accomplish some bizarre and esoteric goal.
So it's semantically ugly in a deep sense -- "what the hell is this doing, why am I reading this code" -- if you do not take the time to 'discover the beauty', to use your words.
But it's semantically pretty in a much less profound sense; the code is pretty straightforward to look through, and the higher-order ideas are built straight out of lower-order ideas, which are clearly partitioned off, and so forth.
Oddly, when I talk to software engineers, most of them just want to create value, ship product, and (hopefully) come out with a readable codebase. Yet when I read blogs or HN, everyone seems to be obsessing over the "value" of exotic languages, 'new' paradigms (usually rediscovered), obscure algorithms, and sample code so simplistic or contrived as to be meaningless. The one constant is that whatever was done ten years ago was pure folly, but luckily we know better now.
Most products--particularly successful ones--take a lot more effort to maintain (both in fixing bugs and adding features long after the initial release) than to publish. You want to minimize the amount of effort needed to add features or fix bugs; a good way to do this is to have shorter, more elegant code.
Additionally, plenty of fields require very robust code. If all you do is share pictures of cats a server held together by Perl and prayer is more than good enough. If you're investing large sums of money based on computer models, you better not have any errors. There is a reason why banks are some of the main users of Haskell.
So: releasing is only part of the game. In certain cases valuing a faster turn-around is more important than code quality; in others, the cost of maintaining a poor code base far outlives the advantage of releasing early.
However, there is another problem with this article: elegant code and "focusing on the application" are not mutually exclusive. If anything, writing elegant code will probably save you time, let you reuse more code and spend less time debugging.
Hence, readable codebase. Everything else you mention is part of creating value. Code that crashes or breaks regularly does not provide significant value.
Because, apart from creating Business Value, some of us actually like the process. It's often the act of building, not the final product, that gets people to become software engineers. So please, cut us some slack here. More fun = motivated (happy) developer = better developer = more business value.
The way I see it, creating value is a nice by-product. It may be what pays the bills but it's not the "fun" part, and certainly not the reason I went into programming / software engineering.
The subset of software engineers who code for the sake of coding frequently obsess over understanding their discipline and stretching their mind.