Hacker News new | ask | show | jobs
by microtherion 2228 days ago
> Make your code simple, dumb and verbose all the time

This is a frequently encountered argument, and sure, if you look at any single line, it looks very obvious what it does. But I would argue that verbosity and lack of abstraction has severe drawbacks for a programmer's ability to understand the overall codebase, and it is disastrous for long term maintainability.

You start out with 20 identical pieces of boilerplate code, and a few years later, you have 20 subtly different pieces of code. Good luck guessing whether the differences were intentional, or accidental. Good luck refactoring the code.

3 comments

I think you confuse simplicity with overt verbosity.

There is no reason simple code can't have higher-level constructs. The interface to them is just likely very domain specific, and you don't start with them.

But after you notice you are copying the same sort of code to a third place, you usually notice a pattern, extract that pattern (with no abstract frills attached) to a unique implementation that can be used everywhere, and move along.

"no abstract frills attached" I can't tell what you think a frill is, but I can't square that statement with the rest of your post, and with the OP. Higher-level constructs might require language features like template metaprogramming, or a level of indirection. There are a few ways in which you can have "the same sort of code".
A 'frill' in this case is any construct that is not obvious to a person a who has programmed a few years in the particular language. For example, this definitions makes most C++ templates used outside of STL-like usage very frillic.

"Higher-level constructs might require language features like template metaprogramming, or a level of indirection."

I think we have different definition what a "higher level" means. To me it means a particular pattern has been identified in the code and lifted to an implementation that needs less thinking and fewern lines of code.

You can have quite high level clever program logic using the basic algorithmic toolbox - the basic containers and large zoo of well known algorithms to operate on them - the array, the list, the map and the graph.

> To me it means a particular pattern has been identified in the code and lifted to an implementation that needs less thinking and fewer lines of code.

To give you a sense of how I think about this, we can just focus on control flow. One option for control flow is very uniform and easy to grasp for anyone, including a programmer from 1950: there are [conditional] gotos and labels. Another option is if/else, while, for, try/catch, yield, return f(),...

So what gives? Particular patterns of gotos/labels were identified and lifted into a situation that needs fewer lines of code. Does it need less thinking? That's where it gets tricky. It's obvious to me that the programmer in the 1950s will look at try/catch and require much more thinking than if they just had goto/label code in front of them.

Template metaprogramming (I don't understand what usage is distinct from STL-like usage) is exactly about identifying a particular pattern and literally lifting a concrete type into a type parameter so that now you have a related family of code. Analyzing the code requires higher level and lifted thinking, the same way that manipulating algebraic expressions instead of concrete numbers does.

That's what I mean by higher level.

I agree that code should strive to reuse "fundamental" container types (implementation, or at least interface), but I don't see the connection to the current conversation, aside from the feeling that using those containers without lifting the types (whether in your mind, or in the language) is impossible.

> Template metaprogramming (I don't understand what usage is distinct from STL-like usage) is exactly about identifying a particular pattern and literally lifting a concrete type into a type parameter so that now you have a related family of code.

I believe GP was referring to the myriad techniques for using templates not as containers, but as type-level functions, and composing compile-time programs that rely on SFINAE, variadic templates, andffunction overloading rules to express functional programs in the C++ template language. You can find examples in boost in various areas.

One somewhat spectacular example is boost::spirit/boost::qi, which allow you to define parsers with a DSL directly in C++ (e.g. using `*c` as '`c` 0 or more times').

If you're doing foo a lot, you make a foo() function. That doesn't mean you have to create a pure virtual FoolikeOperation class and FoolikeOperationFactory, a concrete ActualFooFactory and an ActualFooOperation class.
Suppose you do foo a lot. And sometimes you need to do either foo or bar inside of baz.

You can pass a flag to baz, to choose either foo or bar. Now you have a closed set of possibilities. If you want to extend the functionality e.g with a plug-in, or got any other reason you want to avoid committing to the choice, then you either need

1. first class functions, so that you can pass in foo or bar or whatever.

or if you don't have first-class functions, then you need a

2. FooLikeFactory to make a FooLike object based on a runtime value (e.g. read from a configuration file), and then you can call your FooLike object from baz.

I like the quote that design patterns are bug reports against a language. The factory stuff you're talking about doesn't just exist for fun. It solves an actual problem. I hate Java as much as you do, I'm sure, but I value understanding the reason the patterns exist before I decide to just use a language where I don't need to do any of that stuff.

Oh, for sure, you can still find yourself in a situation where that kind of heavyweight design pattern is appropriate. And if you do, then by all means uses it. I think they're just saying don't jump straight to the top of the tower of abstraction when the first step or two up the staircase will do what you want.
Yes, this very much :)
This is the theorical argument, but in practice when is the last time you encountered 20 identical pieces? DRY is so prevalent that pushing just 2 identical pieces is now rare in my experience. Professionally 99% of my code is used exactly once in one place. I’m not a library or framework developer, I don’t want component by default, I just want to implement a business rule in the most simple, robust and understandable way.
20 is probably an exaggeration, but I see nearly identical (long) blocks of code fairly often at work. the project has been in development for a very long time, and there have been periods where people were not terribly disciplined about DRY. once this happens, it can be pretty hard to refactor; like GP said, it's hard to tell what differences are merely superficial and which handle subtle edge cases.

a common pattern I see that leads to this:

1) construct a relatively expensive object. 2) use that object to do A, B, and C (each of which require setting up their own smaller objects)

there are a lot of different places where people want to do A, B, or C, but not necessarily all of them. but people are reluctant to break A, B, C out into their own helper functions because of the cost to construct the object and possibly the very large number of parameters that need to be passed. with enough time/effort, it is possible to detangle this stuff and encapsulate it more sanely, but it's usually easier to just follow the existing pattern.

DRY is so prevalent that pushing just 2 identical pieces is now rare in my experience.

I want to work where you work.

I've seen literally 4 separate implementations of the same UI component in the last week. It's obvious that they all started out from a common base (e.g. by looking at variable names, function names, etc). However, over time, each component has diverged, as each project that the component was copy-pasted into just made changes willy-nilly to their copy of the component, rather than recognizing that they have a copy of a shared component and refactoring changes upstream or building common abstractions upstream.

Now I've been tasked with doing the refactoring work to make these components DRY, and I can already tell that it's going to be far more work than management has anticipated.

No coding approach will ever solve an issue caused by a lack of discipline.
I no longer think that saying is helpful.

The problem is that every problem is caused by a multitude of causes, and there’s usually no clear way to distinguish issues caused by lack of discipline from issues caused by the wrong coding approach or style.

Different styles make different demands on how disciplined a programmer must be in order to write correct code. My experience in well-managed large code base with decent-size teams is that the recommended approaches and coding styles will evolve to address problems that could be solved by asking programmers to be more disciplined.

So that’s the problem with saying “no coding approach will ever solve an issue caused by lack of discipline”—it only makes sense if you already have a good idea of which issues are caused by a lack of discipline, and if you already understand that, then the saying doesn’t help you. The key insight here is to understand when changing your coding approach may allow you to write correct code with less effort (and discipline). But this can’t be distilled into an aphorism, so it won’t get quoted.

An example is holding a lock to access a certain field. If you’re “disciplined” you can just leave a comment on the field that lock X must be held to access it. But in practice, you want to solve that with code analysis. Another issue is using scoped locks so early returns don’t screw you. You can easily argue that these issues aren’t caused by lack of discipline—but if you travel back in time 20 years or so, “discipline” might be the only tool you have to solve them.

Compiler constantly saves me from errors that I would've made because of "lack of discipline". That's the main purpose of various coding abstractions: to make coding errors that you would spend your "disciple" points on compilation erros instead.
Broadly, that's not true. The correctness benefits of type safety could also be provided by discipline alone.
Not so much 'discipline' as mastery. You have to know your trade, and there will never be a way around it.
The mastery comes in at the specification level, though -- or at least, it should.

Ideally, a program's specification and implementation would be one and the same. The more the program departs from a plain-language specification, the more room for error exists, and the more discipline is required to avoid those errors.

IMO, what we need are better specification languages and better tools to compile them, not better programming languages. I believe we've gone as far as we can go with the latter. Some might even say that watershed was crossed in the COBOL era.