Hacker News new | past | comments | ask | show | jobs | submit login

I'm curious about what I view as two contradictory points in the article: If I create my own language/syntax layers as I go through the various layers of my program, how does that make it more maintainable?

This obviously works for the single programmer, who understands the code he's written (assuming he/she hasn't been away from the code too long), but what happens when you have to maintain someone else's code? If I have to jump into a particular spot on the code to solve a problem, how do I know you've defined <+- as some special monad?

As a maintenance programmer, it seems like I'd have to learn a whole new language with each program I have to maintain. (and I, the hypothetical maintenance programmer, am probably not as smart as the original programmer).




"If I create my own language/syntax layers as I go through the various layers of my program, how does that make it more maintainable?"

It is isomorphic to "creating an API". It's really more a perspective on the situation than a literal description of what's going on. No matter what you're programing in, you're building up some sort of language that subsequent programmers will have to understand to work on your code.


From On Lisp by Paul Graham (pages 59-60):

If your code uses a lot of new utilities, some readers may complain that it is hard to understand. People who are not yet very fluent in Lisp will only be used to reading raw Lisp. In fact, they may not be used to the idea of an extensible language at all. When they look at a program which depends heavily on utilities, it may seem to them that the author has, out of pure eccentricity, decided to write the program in some sort of private language.

All these new operators, it might be argued, make the program harder to read. One has to understand them all before being able to read the program. To see why this kind of statement is mistaken, consider the case described on page 41, in which we want to find the nearest bookshops. If you wrote the program using find2, someone could complain that they had to understand the definition of this new utility before they could read your program. Well, suppose you hadn’t used find2. Then, instead of having to understand the definition of find2, the reader would have had to understand the definition of find-books, in which the function of find2 is mixed up with the specific task of finding bookshops. It is no more difficult to understand find2 than find-books. And here we have only used the new utility once. Utilities are meant to be used repeatedly. In a real program, it might be a choice between having to understand find2, and having to understand three or four specialized search routines. Surely the former is easier.

So yes, reading a bottom-up program requires one to understand all the new operators defined by the author. But this will nearly always be less work than having to understand all the code that would have been required without them.

If people complain that using utilities makes your code hard to read, they probably don’t realize what the code would look like if you hadn’t used them. Bottom-up programming makes what would otherwise be a large program look like a small, simple one. This can give the impression that the program doesn’t do much, and should therefore be easy to read. When inexperienced readers look closer and find that this isn’t so, they react with dismay.

We find the same phenomenon in other fields: a well-designed machine may have fewer parts, and yet look more complicated, because it is packed into a smaller space. Bottom-up programs are conceptually denser. It may take an effort to read them, but not as much as it would take if they hadn’t been written that way.


I think bottom-up programming is great, but I have a bone to pick with that On Lisp quote.

"So yes, reading a bottom-up program requires one to understand all the new operators defined by the author. But this will nearly always be less work than having to understand all the code that would have been required without them."

Here's the problem: it's hard to build good abstractions. Every abstraction has a cost and an overhead in learning it, getting used to it, managing the cognitive overhead. The binary black-and-white phrasing of this argument utterly sidesteps any mention of the tradeoffs involved. Most abstractions we encounter in real-world code fail to take into account the cost of using an abstraction. You built it, you're used to it, you can't empathize with others who need to learn it.

Here's my stab at articulating the tradeoffs: http://news.ycombinator.com/item?id=2329613

Common Lisp is a great language, but it's littered with crappy abstractions: too many kinds of equality http://www.nhplace.com/kent/PS/EQUAL.html, an inability to override or extend coerce, redundant control abstractions like keyword args to reverse traversal order (http://dreamsongs.com/Files/PatternsOfSoftware.pdf, pages 28-30), the list goes on and on.


There's even worse than abstractions that are used by their creator only: those that are not used by their creators at all.

I see that in my code all the time: my abstractions tend to suck until I use them myself, at which point I fix them.

The problem is, we often have to build abstractions that others will use before we use them ourselves. At that point we're kinda stuck, because the necessary changes will break code, and that's scary.


That perspective is, I think, most applicable to Lisp. Because the syntax is so simple, language-level constructs in Lisp appear identical to user-defined constructs - it's all just macros or functions. From the perspective of someone using what you've provided, there's no difference between calling your functions and calling Lisp built-in functions. The ideal is that the application logic will be relatively high-level, only calling these utility functions.

Note that this is different from overloading the same name to mean different things depending on context.


"As a maintenance programmer, it seems like I'd have to learn a whole new language with each program I have to maintain."

I used to think the same thing. But I'm starting to realize that you have to make some assumption about what tools future readers of your code have at their disposal, and that these assumptions change the cost and overhead of abstractions.

Here's a degenerate example. We all know that where you draw your function boundaries hugely impacts readability. You don't want one single function for your entire program, and you don't want every function being one-liners either. There tends to be a sweet spot that takes into account how often a code fragment is needed before extracting it into its own function. But if you take into account that a java programmer reading it in his IDE can jump to the call with a keystroke, that tends to move the sweet spot downward. Now I need less reuse to justify method extraction.

A less degenerate example: ruby has monkey-patching, where you can change a function or class's behavior without touching the file where it's defined. This tends to be viewed as a bad thing because now it difficult to visualize a function's b does. But I'm starting to realize this depends on how hard it is to search for the function. It's painful when you have a load path and have to search multiple disjoint directory trees for a monkey-patch. But it would be fine if everything's in a single flat directory.

I've been working on an arc-like language, and I find I can be extremely promiscuous with my monkey-patching extensions because the language implementation is designed from the ground up to live in the same directory as your app, and finding monkey-patches takes just a simple grep. (Hopefully it's not just that it's just me hacking on it.)

Here's my implementation: http://github.com/akkartik/wart


Related submission today about overheads imposed by abstraction boundaries: http://news.ycombinator.com/item?id=2330206




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: