Semantic Compression(caseymuratori.com) |
Semantic Compression(caseymuratori.com) |
There was a particular Zen, a flow, about top-down programming. You roughly knew the next thing you should work on, and the conceptual stack of things you had to remember were tracked easily by the stack of procedure names.
This is fairly close to what the author is talking about. "OO design" has been a disaster, but as a basic tool of coupling data with behavior, it works fine. If a class happens to match a real-world thing, well that's just a coincidence. A programmer's job is to get the computer to do what you want, not somehow model the world.
At the same time, the right abstraction can make all the difference. While tactically, my process is very similar to the author's, what turns workmanlike code into true craft is finding the right concept that dramatically reduces cognitive burden on the programmer.
Understanding what kinds of abstractions are available to us and how to apply is important. There is a proviso however, and this is where even though two problems may seem to be similar and could be usefully use the same abstraction, one must understand the problem space ramifications of those abstractions as the divergences can often come back and bite you.
Over the decades, many tools have been developed and each of them has some use in our toolbox, whether it be top-down design, bottom-up design, refactoring, objects, values, functional programming, assembly programming, static typing, dynamic typing, etc., etc., etc.
They do not have universal application, unless of course, your favourite is a hammer and everything is a nail. These tools allow us to solve different kinds of problems in a less laborious way.
If anything, the last forty years has shown me that, as a whole, each generation of programmers is unable to learn from the previous generations. We get so caught up in our various wars over which languages or techniques are the bee's knees that we often forget that we are supposed to become craftsmen and craftswomen, able to solve the problems placed before us using whatever tools are available.
Each of us will have favourite tools, but we had all better be prepared to be competent and pick up whatever tools we are given and solve the problems before us.
"Manager" isn't a verb.
</nit>
This so exactly mirrors an ongoing debate in cognitive science. Programmers and philosophers should be talking about representation versus affordances and situated cognition. Actually I suppose the same thing goes back to Chomsky vs Skinner.
"...the right abstraction can make all the difference."
Word.
I developed my own heuristic: If it's hard to explain, you probably did it wrong.
Meaning: The right mental model can make all the difference.
Multiple times, I have revisited complicated user facing problems, simplified things, which improved the UI, the implementation (code), docs, etc.
In the java world, people try to make their entire program out of data structures, even complex data transformations, and unfortunately it ends up being a disastrous case of square peg in round hole.
Comparatively, the code he ends up with is hard to understand. If I was new to the code, I'd be wondering what layout.window_title(title) does. I'd have to lookup the definitions of Panel_Layout data structure and window_title() function. Instead of reading linearly, I now have a tree traversal to do. That is harder.
However, he's not wrong. The changes he made allow more things to be added to the UI panel without the code becoming an incomprehensible mess.
Let's call the original code the linear one. I think the reason his refactoring is good is because the linear function would otherwise have passed a threshold where it was too complex to fit into the working memory of the programmer. The tree traversal exercise he is forcing the next programmer to do serves a purpose. Each function and data structure he introduced is small enough to fit in the programmer's working memory. After looking up those definitions, their (simple) behaviour should be able to be parked in the programmer's medium term memory. Once that investment is made, use of working memory is reduced, freeing up the possibility of adding more stuff to the UI panel function without it passing the threshold.
There's a trade-off. In some ways he made the code worse. But it was necessary. We should always be asking ourselves if the abstractions we are introducing are necessary. Understanding the constraints of programmer's brains is vital to being able to make that judgement.
I wholeheartedly agree with the contents of the article, but I have to hold my nose while reading it.
Obviously plenty of architecture astronauts and cargo cults "refactor" their stuff and produce piles of SpaghettiOs (still spaghetti, but in nice little modules), but that's a straw man, isn't it?
Each of these is great until it isn't. The real magic is in anticipating what you will need and making the switch from one to the next structure.
Who knows, it might one day be possible to have the same code displayed as either:
give john a cookie give jack a cookie give sam a cookie give tina a cookie
and
giveCookies(john, jack, sam, tina)
and
giveCookies(testSubjects.living)
Only visually depending on the number of living test subjects without the underlying code changing at all.
How we are to accomplish this I leave as an exercise for the reader.
Class-oriented programming is used to define new types, like std::vector for example, whose caller know about the existence. This is where the coupling between data/logic happens.
Object-oriented programming is all about interfaces (abstract classes) and tell-don't-asks (in one word: 'messages'), and is a tool to define boundaries between modules.
The author seems to rant about class-orientation. Let's not throw the baby with the bath water.
"Well, OK, if you do it this way it kinda works. But design, design is absolute horseshit"
Give it another decade or two, and maybe he'll encounter a code base that justifies using OO design as well, and he'll write a new article (on how OOD is horseshit, except when you do it his way :).
It's almost as if you choose different tools for different purposes.
Language and program evolve together. Like the border between two warring states, the boundary between language and program is drawn and redrawn, until eventually it comes to rest along the mountains and rivers, the natural frontiers of your problem. In the end your program will look as if the language had been designed for it. And when language and program fit one another well, you end up with code which is clear, small, and efficient.
Obviously, as pg points out later on, this is much easier to do in lisp, but that doesn't preclude you from using this style in other languages
Author does nothing of interest. Writing lots of boilerplate and then figuring out how to reduce it seems wasteful. And calling the alternative to his approach OOP would be a false dichotomy.
You can do the same with much less mucking around with boilerplate by creating data classes first and business logic classes later.
In Delphi you have best of both worlds. You can create a form and when you inherit from it, you can re-arrange all the UI elements and add more to your liking. If inheritance means "I want to reuse these properties" OOP works well.
That's exactly what the author is trying to argue against. The authors point is that because business logic changes, and that because you can't always be omniscient, it's hard to write the correct abstractions the first time. So although it sounds wasteful to write boiler plate and then abstract, it's less wasteful than abstracting, then coding, and then later having to change it all because you got it wrong.
And they already have a widely used term for compressibility in code. It's called "DRY".
At one point I even thought he might conclude with that despite a very semantically compressed solution he reverted to the original solution but with a minor twist and some lessons learned - would have been a lovely twist :)
In this case it probably was very worth while since it was so commonly used. These kind of simplifications are often riddled with trade offs that you get better at identifying and making informed decisions about as you mature and gain experience.
I do agree with pretty much everything written, though I do feel that it is hard to avoid writing bad code in the beginning (but we can certainly improve on it!).
Because they haven't yet spent 30 years watching those techniques being used successfully only to read that, in fact, they obviously don't work at all.
-- struct with three fields
data Bar = Bar { x :: Int
, y :: Int
, z :: String
}
-- function from Bar to Baz
foo :: Bar -> Baz
foo (Bar _ _ z) = ...
`foo` takes a whole `Bar`, but in the pattern matching bit you explicitly say you don't need x and y by using `_`.Also, by enabling an extension[0], you can instead do:
foo :: Bar -> Baz
foo (Bar {z}) = ...
Which I guess gets pretty close to what I want.[0] NamedFieldPuns