Almost a year ago (evidently), I wrote briefly about Abstraction, Modularity, and Composition as important principles in designing software. I’ve had a few more thoughts to add about abstraction.
There are two sides to the abstraction “coin”: there’s (for lack of my knowing of any better terminology) pattern abstraction, and then there’s property abstraction. Each of these are simple enough to be illustrated with just the lambda calculus itself as an example.
What prompted me to write today was that I saw a claim that macro assemblers are really all-powerful and all these languages we’ve invented are “really” just macros piled on assemblers. This is, of course, bullocks.
This amounts, essentially, to the claim that the only abstraction that matters is pattern abstraction. Pattern abstraction is pretty much just taking commonly repeated patterns, and creating an abstraction with the varying parts as parameters. So, if we find ourselves iterating over lists of things often enough, we might go write:
apply f (h:t) = f h >> apply f t apply f  = return ()
And we’ve now abstracted away this iteration pattern, and we can now just write the function we want to run on each element of the list, and re-use the rest. Identify the pattern, and abstract it out.
Applying this to macro assemblers, and it amounts to the claim that class definitions and such from higher level languages only matter in the assembly code they cause to be emitted. The pattern is the only thing that matters.
But that’s not all there is to it. In particular, with our apply function. we’ve abstracted away an iteration pattern, but when can we use it? For such a tiny little two line thing, maybe this isn’t a big deal, we can just go read the code. But for bigger and bigger abstractions, “just read the code” isn’t good enough anymore. We could go read the documentation, but when we have more and more abstractions, “just read the docs” isn’t really good enough anymore either. Read what docs? Should we know everything about everything before we write any code?
We start to see the beginnings of property abstraction when we look at static types:
apply :: (a -> Action) -> [a] -> Action
(I’ve simplified this a lot, because I don’t want this to be about Haskell. Just read “Action” as a return type like “void” in C, where the point is to have side effects, not actually return something.)
Before, we could look at that pattern in a purely untyped way. We could pass anything into it, and the only way to know what would happen is to run it (or at least, simulate running it in your head.) But now we know some properties about the function, and about its parameters.
Abstractions always have an “outside” and an “inside” and both matter, and both have properties we’re interested in. An “abstraction” is not merely a repeated pattern like “apply”. It’s also something we’re holding abstract about which we know some properties, like “f” within apply. (And in some sense, “apply” for the rest of the program.)
My point of this isn’t that static types are great, or whatever. It’s that properties are great. The more we actually know about the stuff we’re working with, the less horrible programming is.
I think “property abstraction” is the main reason that, for example, Java has succeeded, while Lisp has languished. Everyone likes to make fun of Java, but despite its flaws, I do think it’s a good thing that, in Java, everyone knows it’s just plain not possible for an object to suddenly mutate what class it is. Lisp advocates go on about the “power” of being able to do something like that, but I think they’ve forgotten the “power” of knowing that sort of thing can’t happen to you. (Ditto Python vs CLOS, if you want an example without static types.)
Anyone who thinks languages are just macro assemblers is making the same mistakes. They’re looking only at the patterns, and not the properties.