Minimalism can bite you, too. We have around 30 microservices written in Go, each one one of them living in their own repository (separate codebases owned by different teams). Initially we followed Go's idiom "a little copying is better than a little dependency". So every team simply was borrowing code snippets from other microservices' codebases without creating a common shared library, because those snippets were "trivial". Then it hit production and we found that under load/diverse user input many of those trivial functions were quite buggy or inefficient with edge cases. Since those functions/classes were copypasted over multiple codebases, now you had to contact owners of 30 repositories and coordinate bugfixing in all of them, which is slow and painful. Now we're back to having a shared library of common functions, because it's well tested and it's easy to fix bugs by just upgrading. Often what seems trivial is not trivial at all.
There's a long-standing trend of inflexible dogmatism that's pervasive in software development. If some thought leader, blog or Google/FB/Microsoft/etc said to do something in one way, then things must be done that way.
I once had someone get red in the face and call me stupid because I suggested that hiring more employees to run things after-hours would be preferable to forcing developers to spend their free time working on-call, because apparently Google's SRE bible says that developers must support operations themselves, even if that means they're doing free work in the middle of the night after putting in their 40+ hours a week in. That was unacceptable because Google said so, who cares if employees are leaving in droves and those that remain are spread so thin that their contributions during actual work hours were suffering. Google knows best, we must do what Google says to do, even if that tanks productivity and retention. The company eventually hired teams in different timezones to keep things running 24/7.
We follow this too, but additional work is paid. It kind of makes sense, because developers who own a service can solve a problem faster and with a better solution than someone from a different team, because they know the full context. In practice, I had to work at night only a few times when production was down. If it's not supercritical, it can wait till the morning.
The company didn't push for on-call duty at all for our EU developers, because that would cost the company more money for higher rates and bonuses. Instead, on-call duty was the responsibility of US and Canadian workers, because the company didn't have to pay them for the extra burden and loss of free time. EU developers worked on the same products, and could have done on-call duty with us, but that was never on the table for reasons that had nothing to do with developers being familiar with the projects they're supporting. It was all about reducing costs and getting free work out of developers dressed up as "best practices" from Google.
The ridiculous thing was that it was only about a 1 in 10 chance one would get called, but I still couldn't get any sleep on those nights and would rock up even more tired than usual for work the next morning.
The callers would be end-customers from USA so I was always stressed I'd be too blur to answer well right after wake up and give them a bad impression. Didn't help I only understood some parts of the system so would have to muddle through the rest. My spouse wasn't overly impressed by having the phone randomly ringing at 3am either.
I guess it depends on your idea of minimalism.
To me, having 30 microservices sounds tremendously complex. The minimalist in me would want to see those 30 microservices converge into a single project.
If all potential problems with code could be noticed on spot, we wouldn't have the concept of "bugs". Yes, I mentioned minimalism in my post, but I was specifically referring to the idea of copying "trivial" code as opposed to using a dependency.
And did you solved the problem "properly"?
Odds are you did not.
I mean, it's terribly easy to come up with all sorts of two-liners if you just ignore all requirements and constraints, and don't pay attention to usecases.
This is a major problem plaguing software development. Plenty of people act like they have to carry the burden of being the only competent and smart individual in a sea of fools, and proceed to criticize everything that everyone around them does, for the sin of not doing something that exactly matches your personal opinions and tastes.
Best example are C++ stdlib classes like std::vector. Sometimes you just need a trivial growable array implemented in a few dozen lines of code, but std::vector pulls in roughly 20kloc of code into each compilation unit, and only if you're very lucky the compiler will condense this down to the same few dozen lines of actually needed code (the fabled "zero cost abstraction").
IMHO OOP languages are more prone to this sort of code bloat problem because they require (or at least "expect") to write a lot of "ceremony code" which is entirely unrelated to the actual problem (such as in C++: constructors, destructors, copy-, move-operators, etc etc... and the actually important "two-liner" problem-solving method is completely buried).
This is actually a very poor and ill-thought example. C++'s standard template library is expected to provide generic components that are flexible enough to meet practically all conceivable usecases, so that they meet anyone and everyone's needs.
This, obviously, means it supports way more usecases than the naive implementation you can whip out with "a few dozen lines of code".
There are very good reasons why everyone just picks up std::vector over any other implementation, and only extremely rare edge cases (like stuff that sits in the hot path of some games) ever justify using something exotic.
...and that is exactly the fallacy with the C++ stdlib design philosophy, one-size-fits-all classes like std::vector should either be split into several much more specialized classes, or its runtime behaviour should be much more configurable (but ideally both) - in any case there's no justification for pulling in such an enormous amount of code into each compilation unit.
Can you specify what exactly your problem with that is? So far you've handwaved at "I don't trust the optimizer to produce good code", but loc is not inherently tied to how many user-facing abstraction layers there are. In fact, you'll find that there's maybe one additional layer of data abstraction in the actual std::vector itself. There's not much to cut through for the optimizer, and in my experience it has zero trouble doing so.
In terms of compilation speed, you pay the frontend cost for those 20 kloc exactly once if you use precompiled headers.
> should either be split into several much more specialized classes
Could you elaborate what "specialized" (or runtime-configurable) versions of std::vector you are thinking of? Just the fact that some people care about exception guarantees, and some people care about move construction, and some people care about custom allocators, and some people care about emplace semantics, and that not all of these are the same people, doesn't mean that it's inherently good to have separate classes for these aspects.
Only some of the people coming onboard never got over the shock of such a rule, but for the most part, all developers embraced the different paradigm. Teams inter operated faster, iterated changes faster, & there were less issues with inter department libraries. Bugs were far fewer.
It really gives you freedom to have complete ownership of your code, rather than 100% relying on boilerplate libraries. I now see std:: as a disempowering limitation to modern development.
Of course, YMMV.
It also makes it easier to work on other platforms, like embedded, where resources are a premium. Writing your own vector & hashmaps then become part of your muscle memory & it's no longer daunting to write a custom allocator.
It's hard for me to come up with a scenario where even a single one of these follows from not using the STL. How do you "iterate changes faster" when you re-invent vectors and hashmaps so much that it becomes muscle memory? How does each team writing their own containers mean "teams interoperated faster"? How does the STL cause "issues with inter department libraries", and what bugs were caused by using standard library containers? At which point did you ever have to debug a std::vector?
I'm genuinely interested if you can retell what problems you ran into there. Maybe my creativity is limited, but I'm drawing blanks.
(To be clear, at the scale of Facebook or Google, writing folly or abseil can easily pay off because you can integrate, say, your profiling or debugging tooling more tightly. But that doesn't appear to be what you're alluding to. I'll also concede resource management on embedded devices.)
Compile times mainly. This quickly adds up in a C++ project using the stdlib and gets worse with each new C++ version, it's almost at a point now where each stdlib header includes everything else from the stdlib.
> Could you elaborate what "specialized" (or runtime-configurable) versions of std::vector you are thinking of?
First and foremost more control over growth (e.g. when growing is triggered, by how much the memory is grown). More control over what erasing an element means (e.g. whether the remaining elements are required to stay in order, or if the gap can be filled by swapping in the last element). A POD version which is allowed to replace piece-wise memory operations with bulk operations (here I'm actually not sure if optimizers are clever enough to replace many unique moves with a single memmove). An more straightforward way to define an allocator (most importantly, the allocator shouldn't be part of the vector's type signature - not sure if that's what the new polymorphic_allocator in C++17/20 is about though).
...those are just some obvious requirements I had in the past, but I'm sure other people will have different requirements.
I once had a coding problem interview where a half of the logic could be handled by an autobalncing tree. I\ve never really used autobalancing trees in real software before, but i knew how to make them from scratch, quickly as RB tree is a very common school problem. I spend twenty minutes choosing between coding one from scratch and picking an already existing solution. I ended up choosing gnl's RB trees, with all the makefile/autoinstall issue that i would have to fix instead. I did not gain any time, really, but i wanted to show i did not suffer the NIH syndrome. Was that a mistake? Should i stay within the stdlib during coding interviews (i don't know if they could run the code, i think the interviewer was running windows)
The problem is that both are right.
(but jokes aside, I guess that the interviewer is more interested in your ability to solve a problem from scratch instead of you ability to google for an existing solution - even if googling makes a lot of sense in the real world)
And I use FreePascal. Its stdlib is vastly larger than the C++ stdlib, but also completely untested/unusable, because no one is using it.
I wrote my own unicode handling functions for everything.
Yesterday I found a new test case, and noticed that my convert utf8 to lowercase function did not handle the Turkish İ correctly (it should turn into two codepoints rather than one for reasons. although I had a check for that symbol, but it was in the wrong branch). And my function had quadratic runtime, so it was also nearly unusable.
So I fixed my function. Then I thought, why am I even writing a convert to lowercase function? FreePascal already has a convert to lowercase function. There is a lesson here, do not write your own functions, you will miss cases.
So I loaded the stdlib Unicode functions to compare my function to their function. And, segmentation fault. Not in the convert to lowercase function, but just loading that Unicode part of the stdlib broke something.
Although while writing this post, I thought, perhaps I test it again, just the convert to lowercase function, without the crashing part. It also fails to handle the İ. And worse, it returns a string that is one byte too large, like a garbage null terminator. There is a lesson here, do not use the stdlib, it is just broken.
Then I compared it to a compare to lowercase function from another library I had included. Twice as fast as even my new fixed function. But it also does not handle the İ symbol. There is a lesson here, do not use other libraries, they do not do what you need them to do.
I would say something like: "I think a balanced tree, such as an rb-tree, would be useful here for <reasons that make sense given the problem and the properties of rb trees>. I've written rb trees before and think I could write a basic one in 10-15 minutes or I could use <class from the std library, which uses a balanced tree>. Which would you prefer?"
Assuming what you said made sense I would take an interaction like that as a positive signal.
The code consists almost entirely of templates and inline functions and every modern C++ compiler will optimize that away. In a release build there is basically no overhead compared to using raw pointers.
https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
Things like this:
static final int INT_1 = 1;
I've actually seen production code with: static final string HTTPS = "https";
static final string COLON = ":";
static final string SLASH = "/";A day later the Java guy got back to me with a full blown Java project three levels deep which was just a translation of my ~30 lines of Python, and it “didn’t work”. I had to read what was now hundreds of lines of Java to find 3(!) bugs in it.
That day I realized FizzBuzzEnterpriseEdtion is much closer to reality than I thought.
As is, the only way I'd approve of it is if it was required to have the value be addressable. I'm not sure how often of a concern that is in java, but in Go for example, from ~1.3 to somewhat recently sticking an integer in a interface would cause a small allocation to hold the integer. By having it stored as a variable (you can't take the address of a constant in go) one could elide the allocation by storing the address of the variable in the interface instead of it's value. Of course now go will automatically do that for small values (N < 256).
That's a pretty niche use, but it does exist. I'll assume it's not novel, and there are other, perhaps similar use cases for having a symbol instead of using the literal.
wrt https: Yeah, it should be in muscle memory, but humans make mistakes, fat fingering is a thing. Using a symbol can give you a compilation time check. Of course, you may typo the symbol and have the exact same problem. The ability to compare by address actually applies more to strings though, since you can check the address & length for equality rather than having to do a full comparison of the string. This may or may not be an issue in any given language/implementation based on a variety of reasons (string interning, multiple definitions of a literal due to runtime/dynamic loading).
assert(INT_1 == 1);
Gotcha. class MyBetterrrrInteger(MyCustomNumberClass) implements ICalculatable {
public static MyBetterrrrInteger(OtherIntegerClass num) {
...
// oh did I forget to add generics to the mix? damn.
}
...
}https://www.gnu.org/software/hello/
For a while I did not clue in to the fact that they were serious, I thought they were making fun of their own build tools...
In fact, I came across a line of code that was basically `two = 2;` in my own company's codebase just a few days ago.
One thing I'd add over the 'screwed industry', at least in the java,model era is that the more they added, the more "tools" they needed and it was seen as a quality. Basically quadratic complexity at the cultural level..
ps: as usual, https://duckduckgo.com/?q=stop+writing+classes+pycon&t=ffab&...
Maintainable code has sane abstractions, it wraps external dependencies, and it has minimal interfaces between them and other code. Minimal here doesn’t mean quantitatively minimal but qualitatively minimal.
To me, this reads like a requirement, can you really blame them for respecting what you asked for?
One such case I was permitted to write about (most of my work is under NDA), I can't believe it's already been 8 years since then:
I struggle with that all the time. Libraries always tend to accumulate such trivia. My litmus test for it is if the documentation has more lines than the code.
I hate libraries that are a mile wide and an inch deep.
Not sure I am understanding this correctly. Would undocumented libraries be the best or the worst according to this metric?
Sometimes you want a library anyway in these circumstances though, since the expertise required for the short implementation might be significant.
If his solution doesn't work with arbitrary number-like objects (matrices at the very least), doesn't support voice input, can't send results via email and can't seamlessy resume the calculations after a hardware failure, it's far from overengineered.
Should have used TDD/BDD, developed some Gherkin test cases, and used those to develop some JUnit automated tests before starting on the implementation. Oh, and each class -- for the solver, coefficients, variables, etc. -- needs to leverage dependency injection so they can be properly mocked in the tests...
I'm reminded of a story about Kent Beck or somebody, one of the big TDD gurus, who wanted to get a microcontroller to draw something on the screen or something. So he first wrote the program in Java, being sure to employ proper TDD principles, and then hand-translated the Java code to microcontroller assembly or something. But this was feted as a win for TDD and evidence that TDD can be used for anything and therefore should be used for everything. The details of the story are vague, and I can't source it right now, but I know it exists. Some Hackernews is bound to dig up a link, they usually do when I make an obscure reference.
I don't know how your two liner worked nor why it was deemed improper. I don't know whether it solved all special cases. But it in fact failed your political goal.
AFAIK, the closest you can get to free functions is a non-static class that only has static functions. infuriatingly, many coverage tools will count the "constructor" of this class as uncovered code. stuff like this makes me really miss C++.
Static inside inner class is something else entirely and has nothing to do with topic of this thread.
But the 80-20 principle still holds true for many things — the rare edge cases may indeed bring a whole lot of complexity with themselves. They can easily be a difference between an O(n) algorithm and an NP one.
Minimalism is okay (in that we should strive to decrease the accidental complexity), but I found that some people overdo it to such a naive and frankly, dumb levels that it is actively harmful.
Everybody came up with some solver classes in Java, some stateful, some stateless. One guy presented the most monstrous solution with 100+ lines of code where you could individually assign the coefficients, individually modify them, trigger recalculation, get the number of roots, get the roots. That guy was very proud of what he did.
Oh, I found bugs in his code. But that was considered normal. People make mistakes, right?
I showed them a two-line solution with a single function (where you'd try hard to make a mistake) and nobody liked it. I was ridiculed right there, for not solving the problem "properly".
It struck me how someone can overengineer a solution to the simplest problem and be proud of it. I thought at the time this industry is screwed if this is the norm.
In fact probably some 90% of so called library code on GitHub is overenginnered crap. 90% is arbitrary but from my experience every time I look for some solution let's say for some GUI effect on iOS, find a library or a framework, take the source, analyze it, start simplifying it and I end up with a version that is orders of magnitude shorter and can be just copied into my project it's so trivial.
You realize that it was so trivial that there was no need for a library in the first place.
I liked the post about minimaism the other day here on HN [1]. It still amazes me how minimalism is not the norm, is not taught as the only way you should solve problems in software engineering. There's no "proper" way other than the most minimalist one, period.
However, minimalism requires some extra effort to achieve, and that's the whole point of engineering.
[1] The post was about minimalism in programming languages, but the author had another, more general post: https://pointersgonewild.com/2018/02/18/minimalism-in-progra...
Edit: mandatory favorite essay: http://www.paulgraham.com/power.html