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.
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.
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.
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.
"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 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.
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').
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.
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.
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.
Despite the modern c++ version being the highest quality and the original windows-style version being the most fully featured, the vast majority of people use and prefer the rinky-dink basic c++ version. Simply for the reason that you don’t need to be a senior level dev to contribute meaningfully to the code.
I should also add that Hungarian notation is the prototypical example of dumb, verbose code, that wants to make individual lines easier to understand by dragging type information into every single variable name.
If used wrongly. Joel Spolsky wrote a whole post on it[0], but the TL;DR is that you should use the notation to differentiate between variables of the same type. For example, you might have world coordinates and object coordinates in a game script. Correctly used Hungarian notation would denote them, for example, with `wPosX` and `pPosX`. Even though they're both int (or float), you can easily see that you shouldn't assign one to the other.
Using them to notate types, however, as in `iPosX`, is completely useless. I fully agree with that.
[0] https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
That is not something you want in modern C++. Quite the opposite, in fact, and many projects avoid Boost for that reason.
Templates should be used when needed, no more.
- The official server (arcturus) is awful to work with code-wise. But all the decently big private servers uses it because at one point we only had the binaries of it and it worked out of the box. When the source started to leak too, it was easier to continue forward with that thing.
- The "simple version" which I am assuming you refer to is os(i)rose. This was the only thing you would get BEFORE the official server got leaked. It had some momentum simply for being there since roughly 2006. It was based on Brett19's code which at the time was a 14-something teenager. The same brett that now works on a fully modern C++ codebase that is decafemu.
- The Modern C++ version which if I remember is worked by few folks from osrose came out something like 2 years ago. Passing 2015, the momentum for the game is close to none. So yes, no one will even spin that codebase.
The official server was stolen due to horrible code-practice (C++-wise and software engineering in general), like having plain SQL-injections when creating characters. Worst! This was one of the reason that made the company (TriggerSoft) behind the game go bankrupt. The game was full of security holes back in 2005. This made the game's economy being broken due to few cheaters, created few horrible roll-backs and such. This drained the player's base from the game.
The "simple C++ server" osrose was also plagued by security issues and technical issues. Up to a point that people preferred to patch the official server with dll-injections + assembly rather than trying to make this "simple C++ server" work.
Not always, though.
See every bug and exploit with C arrays or pointers that exists because C devs think even minimal attempts at safety are too complicated or slow, or old-style PHP code that builds SQL queries out of printf strings directly from POST values, or probably countless other examples in other languages. C++ code that uses raw pointers instead of references or that uses std::vector but never actually bothers to bounds-check anything.
It's entirely possible for code to be too dumb for its own good.
That can also be perceived as a a flaw of the language design in that it does not allow one to write dumb, safe and fast code. Which are such languages in existence today?
edit: fixed typos
- Dumb: you bet. The inclusion of lambdas has shaken things a little, but usually Java code is straightforward with little space for "cleverness". The counterpoint to this, of course, would be the memes about AbstractFactoryFactoryImplementationSubclassDispatcher class names and all that, which IMHO does not represent much actual Java code out there. There are good reasons why big corps prefer Java, and readability is one of them. As a programmer, I've found it easier to jump into a Java codebase I didn't know much about than in any other language. And this has happened even when I had little experience in Java.
- Safe: yes. You have to go out of your way to be unsafe in Java. Memory allocation is done for you and the try-with-resources idiom is almost as good as C++ RAII.
- Fast: also yes. Usually about 2x or 3x the run time of C/C++ code, some times even less.
As a technology it really didn't have much to give. Object Pascal was born in 1986, Ada in 1980 with language support for design by contract, JIT with Lisp in 1960 and Java came in 1995.
- Too many jumps in code logic instead of serial logic
- Premature optimization 1: messy code (using lots of unclear variables etc), this is common within calculations, and games have quite some of those.
- Premature optimization 2: failing to properly architect
- Single-character variables. Java IDEs default to fullvars
- Bad function/method names
- Not expressing what you mean (for i= vs foreach)
- Too many abstraction layers / IoC
- Too many sideeffects / complex code (interwoven code)
- Callback hell
Nr 1 is important.. You need to be able to "follow the code". If that requires you to create a DSL in order to code something async in a serial way, then by all means do so.
For example with: network (duh.), but also in games: character dialogs, animations etc. etc.
C++ offers "efficient" ways to be clever, with varied and difficult challenges that can be addressed in relatively little difficult code; some are good or harmless (e.g. aligning struct fields to cache lines or concise towers of useful templates) and some are bad (e.g. flaky homemade not-too-smart pointers and almost-STL-compatible containers).
Java, on the contrary, facilitates the creation of large, boring and easy to read generic object-oriented tumors, that become satisfactorily clever only when they go very far (e.g. one more layer of indirection than everyone else) or reach theoretical limits (e.g. nothing left to invert control of).
The inclusion of lambdas has shaken things a little
I'm curious, do you consider lambdas as more or less "dumb"? Because I consider them the dumbest, simplest and maybe best way to do polymorphism. In a way, OOP's whole shtick was about not using them and instead extend stuff with classes.not commenting on the Java part as I believe it's usually faster than that (though not so sure when you see the years of hoops that Minecraft java had to go through to stop being so damn slow all the time...) , but it's kinda frustrating to be fighting for microseconds almost daily and then hear people saying that 2x slower is fast... 2x slower means going from 100fps to 50fps which ... well, gets you fired ?
Using classes for everything with a full OOP approach, instead of DOA and ECS, with tons of new in hot paths, no wonder it had performance isuses.
This was discussed in some Minecraft forums,
https://www.reddit.com/r/programming/comments/2jsrif/optifin...
It basically boils down to
> Why is 1.8 allocating so much memory? This is the best part - over 90% of the memory allocation is not needed at all. Most of the memory is probably allocated to make the life of the developers easier.
HFT is as high demanding as games and it makes use of Java, however I think that Java developers with such skills rather have a HFT salary than what game devs earn on average.
I remember a story of a HFT trading software written in Java. Supposedly it had big issues with GC. That's why they built a system where multiple threads would attempt the same operation, and the first thread wins. This approach reduces the likelyhood of a GC ruining the timings. Funny story.
My bachelor's thesis involved writing a software in Java that would manage dozens or hundreds of millions of small objects. These objects were all instances of the same class; the contained only three ints. It was very slow, and especially in an OOM situation the GC would work for more than a minute before finally giving up. I changed the software to use SOA instaed of AOS - moving from a huge array of these objects to three int[] arrays. Since ints aren't boxed, that left me with only 3 objects instead of many millions. The code was uglier for it, but the performance was another world. Unfortunately, such a change is not practical if you have many classes.
That was 5 years ago with Java 8. Disclaimer: I haven't followed Java since then. I know next to nothing about it.
There is an EA available for Valhalla and there is now the roadmap to incrementally bring such features into the platform. Java 14 has a new native memory support as experimental and it might reach stable already by 15.
https://jdk.java.net/valhalla/
https://cr.openjdk.java.net/~briangoetz/valhalla/sov/01-back...
https://openjdk.java.net/jeps/370
The problem why it is taking so long is engineering effort to keep ABI compatibility, namely how to keep 20 year old jars running in a post Valhala world, while at the same time migrate value like classes into real value types.
Java's biggest mistake, from my point of view, was to ignore the GC enabled systems languages that had value types, non traced references, and AOT compilation from the get go, then again I guess no one on the team imagined that 25 years later the language would be one of the choices in enterprise computing.
Back to Minecraft, the game isn't Crysis or Fortnight in hardware requirements, so a language like Java is quite alright for such game, what isn't alright is what the new development team apparently lacking experience eventually ended up doing to the game engine.
If one is to believe a couple of posts like the one I referred to.
With regards to performance, there must be some fine art in splitting structs into smaller structs, and keep them as parallel arrays, but there is also a limit to it. At some point you will need too many pointers to point at the same position in all these arrays.
I've never cared to split a lot, since it makes code harder to read. My guideline has always been to optimize for modularization: In OOP there tend to be large objects containing links to "all" related information. That violates the rule of separation of concerns. With parallel arrays you get perfect separation of concerns. One parallel array doesn't even need to know that there are others.
> Back to Minecraft, the game isn't Crysis or Fortnight in hardware requirements, so a language like Java is quite alright for such game, what isn't alright is what the new development team apparently lacking experience eventually ended up doing to the game engine.
I'm not in a position to judge, and I've never even played it, but it seems to me that Minecraft has a lot of voxels to maintain. Also massive multiplayer requirements?
As on the client side, proper coding plus offloading stuff into shaders already goes quite far.
And even in the debatable point that Java isn't the best language for any kind of game development, well maybe Mojang would never have taken off if Markus had decided to prototype the same in something else.
Nowadays the Java version is only kept around due to the modding community, as the C++ version is found lacking in this area.
Naturally at some level you will have a class full of native methods as FFI to OS APIs or libraries written in C or C++.
From that point of view, I consider Java still a better option as game engine scripting language as Python/Lua/JavaScript, because you get strong typing, there is still dynamic loading, a good set of AOT/JIT/GC infrastructure and more control over memory layout than those languages allow for.
Naturally that is a matter of personal taste.
Java as a language is dumb, but as an overall platform is not. In a way it is a two-layer platform: you have the outer layer, a boring language that is used by a lot of people to write most of the code, then you have a hidden layer, that most people ignore, made of bytecode manipulation, runtime code generation and language agents that let you do cool things like adding compile-time nullness checks, generate mapping classes to avoid writing a lot of boilerplate code or good old ORMs that automatically generate your sql queries for you.
Such functionalities are not exactly easy and straightforward to use, but in my opinion it is a good thing: they are there and can be used, but for most programmers will be hidden behind a few "magic" annotations.
This is in contrast to other languages where the advanced functionalities are "all over the place" and every programmer must be aware of them (I'm thinking of C++ and Common Lisp for example).
If you have a teams of great programmers you may achieve better results with the latter approach, but for the average company the Java approach is better because you can have average programmers write boring code while taking advantage of a few clever tricks here and there by using libraries/frameworks written by better programmers.
Zig will be there soon.
> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
~ Rob Pike
I don't know that this is, ipso facto, evidence of anything about C programmers other than that they really hate C++, though.
The experience of writing new code in one is very, very similar to the experience of writing new code in the other, right down to the compile times. The lifetime analysis in Rust is nice and pretty far ahead of what static analyzers can do in C++, but Rust Generics are a pretty weak approximation to Templates. Rust has better Browser integration, C++ has Qt. One imagines the languages will catch up to one another on these fronts. C++ has Inheritance, Rust settles for Interface Polymorphism (one can reasonably prefer either).
The one really big difference here is actually cultural - the Rust community all agrees on Cargo, and it's a bit happier to compile the world and distribute static binaries, which removes massive headaches for both the developer and the end user while setting the language up for an eventual, Electron-style tragedy of the commons where a user is running like 8 Rust apps with their own copies of the same 14 libraries resident in memory (but that's a good problem to have because it means you've displaced C/C++ as the linguae francae of native app development).
I guess the other really big difference is that there is no legacy Rust code.
I like C++, but I can understand hating it. But if you have written new code in C++17, and hated it ... I suspect you are going to hate writing Rust too. And if you love Rust and hate C++ ... I suspect what you hate is legacy C++ from 2005.
Finally, I was explicitly not concluding anything about C Programmers beyond that they hate C++.
Rust's generics allow for some seriously powerful abstractions to be built in a very clean and readable way, although there can be friction with stuff that would be simple with templates in C++ and quite verbose in Rust.
Maybe concepts will change that.
Template (at their origins) are nothing more than generics, and a pretty clean, powerful and zero cost way of doing generics.
What you name "hazardous, unergonomic" macros style is not the template system itself. It is mainly due to all the 2005-styles SFINAE hacks that have been invented by abusing templates properties.
SFINAE in C++ is nothing natural, it's at best a dangerous trick to have compile time resolution/execution.
Fortunately all of that should die progressively with C++-17 constexpr for the good of humankind.
C++ also makes it way too easy to reach for shared_ptr instead of unique_ptr leading to all sorts of unfortunate things. Rust makes that much harder and RefCell/Rc/Arc push towards design that are "single owner" which I've found scale out much better once you move into programs that are of a significant complexity.
C++ still wins in portability on some platforms but I have a hard time picking it for anything greenfield at this point.
But, yeah, there are a ton of domains (notably embedded) where I would want Rust.
However with the support of companies like Microsoft, Rust will eventually get there.
By the way BUILD 2020 will have Rust sessions.
When is the last time you accidentally mutated a raw pointer? My opinion is that references are just another C++ feature that solves a non-existent problem and has severe disadvantages. And that is non-orthogonality / combinatoric type system explosion. I've seen more than one codebase that consisted to a large degree of adapters for calling the same functionality.
Yes I have seen cases where classes or functions are unnecessarily generic, adding templates when you only needed to support a specific type (YAGNI).
But in the end, most bad code I look at, I completely understand it's syntax. It's the semantics of this so called "dumb" code that prevent me from modifying it or fixing a bug in it for days until I actually understand the rats nest of ideas expressed in the code.
I think using features like using const as much as possible, preferring return by tuple rather than multiple in out parameters and a bunch of other modern C++ features more often than not make code bases simpler than the other way around.
Fix a bug somewhere ... pray to the Nasal Demons you got there before more than two developers duplicated it elsewhere.
y = (f(x1), f(x2), f(x3))
vs y = f(x1, x2, x3)
Not entirely sure anymore though. But it was something about causing side effects but throwing away return values with the ,-operator and involved function calls, I thinkI mean, they're only readable to people who have dabbled in variadic templates in their free time. That's how many people on your (future) team?
This line of reasoning is vacously true for any syntax and semantics though. Move semantics and rvalue reference are only readable to people that have taken the time to understand them -- they're undoubtably useful though.
It is best to use them only in performance sensitive places and containers.
C APIs come, of necessity, with tons of documentation about who is deleting what, when. Or, you know, maybe they don't and you have to learn the hard way. std::unique_ptr (implemented with move semantics) largely solves this problem.
And you can imagine notions of ownership more complex than "I'm deleting this at some point" (maybe "I'm versioning this object now, don't worry about it"). If you want to encode these transfers of ownership into your API, that's Move Semantics!
The point is that you shouldn't be using rvalue ref parameters, std::forward, etc. in most of your code. Even std::move should be fairly rare.
Passing std types across API boundaries is a code smell.
"I have created an object and will pass its unique ownership to you." -> std::unique_ptr
"This routine needs a function that takes two ints and returns a float (without putting all my code into headers)." -> std::function<float(int, int)>.
Can you elaborate in what circumstance you should not pass std::types across API boundaries?
std::function is useful in some situations, but "without putting all my code into headers" is not a good argument.
This is the idea behind Golang, isn’t it? That everything should be written out explicitly and not hidden behind abstractions. Some people love that, others hate it.
It's also much easier to avoid allocations to begin with than C# - pointers can be used as interface types without boxing (and taking interior pointers to fields / array elements is allowed), and it's possible to allocate regular Go objects / arrays in unmanaged memory and feed it into any API.
Obviously it's not really designed for it and the ecosystem isn't there but using it hasn't been completely terrible so far.
C++ can be a real bitch and commit one to a mental institution or it can be very helpful if one does not try to play PhD.
I write servers and some other stuff in C++ and find it incredibly easy to use. I just do not do any esoteric things.
Named parameters, for instance. It's insane that function parameters still can't be specified in any order with name=value expressions. That would have saved numerous bugs over the years, but apparently I'm the only one who thinks so. When a language such as C++ is harder to use than Verilog, somebody has screwed up badly.
The thing is what is complex abstraction changes with time. At one time, floating point, especially transcendental functions would have been a complex abstraction. Functions at one time were a complex abstraction. Classes and polymorphism were a complex abstraction. Pointers are a complex abstraction for a lot of people. Linear algebra is a complex abstraction. Transforms are a complex abstraction.
Many times “complex abstraction” just means “an abstraction I am not familiar with”.
Back in the 80’s most games were written in assembler. I am sure many people thought that C was a complex abstraction. I mean, instead of just doing a jmp to a location, now you had a stack and a calling convention and which registers to save and restore...
Since “complex abstraction” is often code for “unfamiliarity”, education, like what the original article is doing is very helpful in moving the state of the art forward. As people become familiar with new abstractions, that becomes the new baseline for “simple” code.
A lot of times it can also mean "an abstraction that my development environment is unaware of or otherwise has deficient tooling for"
Now not all features are bad, lambdas are OK when used as local functions and can make the code more readable if the alternative is to define some static function outside the current method (e.g. you want to pass some custom filter or comparator). They can certainly be abused though, but it is one of those cases where their usefulness is greater than their abuses (and i can't say the same for "auto").
For the example given... it might be a bad example, but honestly i was looking at that code for a bit and i simply cannot read it - i do not understand what is going on just by reading the code, i'd need to run it in a debugger and go through it step by step (and i've actually written texture atlas packers before). It completely fails to sell me on the "fold expressions" and "parameter packs" and it certainly doesn't look at all "elegant" to me (but note that it might be that the example is awful, not the language feature itself).
And it did make me skim through the rest of the article though since after completely failing me on all fronts at the introduction bits, i couldn't get the feel that i have any common grounds with the author.
var person = new Person();
var car = selectCarById(carId); // Car
if type is not obvious, it should be explicitly declared.`doSomethingToACar(selectCarById(carId));`
Kind of weakens the argument. I'm not sure what's the best approach, but I'm usually ok with autos even when the type is not explicitly known - when reading code, I do not really need to know what exact type a variable has ("it's a car, goddamnit, it says so in the name!"), just how it's used (and then meaningful function names become very important).
for(auto iter = vec.begin(); iter!=vec.end(); iter++)
for(std::vector<project_namespace::class_name>::iterator iter = vec.begin(); iter!=vec.end(); iter++) for (auto i : vec)Especially when that "auto i : vec" should have instead been "auto& i : vec" or "const auto& i : vec" and now you are at best wasting cycles and at worst writing to copies that will soon be discarded, ending up with a bug that can be very hard to spot.
for(Class entry : container)
This is still an uncontroversial improvement over having to typedef or use auto for the iterator.Generation of a single solution: 3 easy lines (calling on a few hundred lines of goofy math that actually describes the structure, but that's common to all of these approaches)
Writing a for-loop to fill a std::vector of solutions -- about 10 lines of a familiar stack-walking pattern which could confuse a novice.
Making a fake container that defines a begin() and end() along with a nested iterator class: about 20 lines of necessary boilerplate, another 20 lines to replicate the stack-walking, now sprinkled about the boilerplate. The novice is completely bewildered, so we add another 10-20 lines of comments to explain it.
So I have this strong urge to keep the first two implementations in place, just to provide a gentler ramp. But I won't use the code in the end, so it would only add maintenance overhead, so a lone tear rolls down my cheek as I delete the clear, readable code.
In python, this is often as easy as changing square brackets to parentheses to change a list comprehension into a generator.
The debate is about all the other cases.
I know i do not use any of the languages you mention, for example - and if i did, i'd explicitly write any type names.
But honestly i can only talk about me here, i can't guess why some imaginary other developer who dislikes a feature does dislike it.
How do you reconcile that world view with the fact that people are shipping billions of line of codes that obviously work in languages where until recently you couldn't even write any type anywhere (JS, Python) ?
For others, it was worrisome because it made code easier to compile, and rightly so. If it complies it doesn't mean it's correct.
I mean, sure, if i take that code and run it through a debugger - perhaps while also crossreferencing the features it uses at cppreference.com - then i'd be able to follow it. However at that point any relevance to readability would have been thrown out of the window long ago.
For me, this whole spread/fold feature is easy to grok, because I've worked with many similar features elsewhere. In this case, the feature looks almost identical to how `...` is handled in one of the Scheme macro systems, syntax-case (and syntax-rules, by extension)[1]. As mentioned, the use of spread on a comma operator with a side-effect is tricky and maybe too clever, but, otherwise, I don't see anything out of ordinary.
> perhaps while also crossreferencing the features it uses at cppreference.com
That's the thing - if you already knew the meaning and syntax of these features by heart, you'd find the code using them very readable. You also wouldn't need to step through it in the debugger, because there's really not much happening there in terms of control flow.
In general, "readability" is simply a bad word to use: it's too overloaded and means too many things to too many (kinds of) people. Every language can become readable to you if you put enough effort into it; and no, the amount of effort needed is also dependent more on your prior knowledge than on the language in question. So it's just too subjective to be useful as a metric for anything, unfortunately.
[1] https://docs.racket-lang.org/reference/stx-patterns.html#%28...
Yeah, but the effect of this is greatly overstated most of the time. As I said, given enough effort, you can learn - and learn to keep in your head - anything. It matters in the short term, while you're learning, but in the long run, once you've learned and internalized all the required information, it stops being relevant.
It's probably harder to learn to read and write kanji instead of the Latin alphabet. For most people, that difference matters for a few years in their childhood, but once they have the characters drilled into them, it no longer matters: they can read and write as well as any Westerner.
The same is true for (natural) languages: some are inherently more complex and hard to learn than others, yet once you become fluent, you stop noticing the complexity. You simply speak, read, and write your thoughts directly, without thinking about grammar and spelling too much.
It's also visible in sciences and engineering. Mathematical notation is especially notorious: not only every symbol can have multiple meanings, but you're expected to also guess which meaning was intended from other symbols and text around. That's on top of introducing hundreds of made-up words for equally made-up concepts, like a "number", or "monoid in the category of endofunctors".
Finally, it manifests in programming and programming languages. In various ways. For example, there are some people who use APL, K, or J - because it's "easier to read and keep in your head a single line of APL than a 500 loc of equivalent C". If given a chance, they will tell you that something like this is of course very readable and straightforward:
⍝ John Conway's "Game of Life".
life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
You just need to learn a few things first, and that may be hard, but once you do - I'm told - reading and writing code this way becomes effortless, and a thousand times more efficient than writing in C.Basically, if you're going to be switching languages every year, then yes, there's a difference between having to learn the language for a month rather than six before you can ship something. On the other hand, if you're going to stick with a language for a decade or two, then the long learning process becomes irrelevant, as it's dwarfed by the rest of the time where you actually use the language.
> it still is hard to follow the flow
It may be hard if you're not familiar with the common patterns of using higher-order functions. HOF and lambdas are not GOTO: there's a structure there, it's just richer than the basic set of if/for/while statements. You could call such a structure an FP equivalent of OOP design patterns.
> calls to other functions call back to local lambdas
Yeah, but that's also true for every abstraction, starting with a procedure definition. Also, you don't need lambdas to have this problem, it's enough to register procedure as a signal handler, or register an event handler in some async framework. When you pass a comparator function to `qsort`, you similarly don't know when and how that function will be called, even though it's a named procedure.
To summarize: no matter the language, you can learn it, you can fit all of it in your head, and you can make it readable for you. It requires effort, which is an investment: it might not be worth your while, depending on your circumstances. However, if you encounter a code you don't understand or have trouble with reading, because you didn't invest enough time into learning the language, that's not the code's (or features') fault. Just be honest with yourself and don't blame others for what is a result of your conscious decision.
Also of note: yes, the features often do differ in their complexity, and the differences influence the readability (for lack of a better word). However, to see this and to be able to compare, you have to first learn the features in-depth.
Edit: also the default comma operator discards its lhs, it is pretty much always used for its side effects.
[1] https://github.com/milessabin/shapeless/wiki/Feature-overvie...
I already wrote that there are some cases where auto is necessary (usually when used with more recent C++ features).
> especially if an IDE is there to expand complex type information
And i also already wrote that this information is not only often cumbersome to obtain but also such an IDE is often not available - e.g. in a web-based code review tool which also happens to be an environment where you want the code to be most understandable.
This usually means types that are from inside the scope of another class. Compound types that are used frequently can actually be aliased.
Also writing programs that are clear when reading from plain text is great, but I don't think that should ever be a higher priority then what it is like to work with inside an IDE. The days of writing programs with notepad are over thankfully. Languages aren't the only way to make programming easier and aren't even where the low hanging fruit is. People get caught up in languages, but tools can help much more without the herculean effort of redoing decades of work, so I lean on them whenever possible.
However, about IDEs, you still ignore that code is not only worked with inside IDEs - i already wrote twice the case of a code review tool... have you ever worked on a team with code reviews done by a web-based tool? Or even with a source control program that you want to check the differences between commits that someone else made long ago (they may not even be at the company anymore) and the diff tool obviously has no idea about types and such?
There are many reasons for why you need to work with code outside of an IDE and none of them have to do with using Notepad to write the code.
It's always "ugh, this code is a copypasta convoluted mess. I'm going to replace it with a simple solution".
It's not that different from the English language either. Laying out your thoughts in a clear and structured way is the real skill. Start using words and expressions that I have to constantly google for, and I will hate you very quickly.
You just need to know functional programming.
Know your tools, people.
I always say it as "write code that you could debug at 2:00 am drunk."
Be simple, do not be clever and be clear.