Hacker News new | past | comments | ask | show | jobs | submit login
In defense of complicated programming languages (viralinstruction.com)
209 points by jakobnissen on Jan 25, 2022 | hide | past | favorite | 370 comments



I remember first encountering classes. I simply could not understand what they were from reading the documentation. Later, I picked op Bjarne's C++ book, read it, and could not figure out what classes were, either.

Finally, I obtained a copy of cfront, which translated C++ to C. I typed in some class code, compiled it, and looked at the emitted C code. There was the extra double-secret hidden 'this' parameter. Ding! The light finally went on.

Years later, Java appears. I couldn't figure that out, either. I thought those variables were value types. Finally, I realized that they were reference types, as they didn't need a *. I sure felt stupid.

Anyhow, I essentially have a hard time learning how languages work without looking at the assembler coming out of the compiler. I learn languages from the bottom up, but I've never seen tutorials written that way.


I understand this thought process, but in my opinion it's the wrong way to think about software concepts. Understanding what a bridge is doesn't mean knowing how to build one, and in fact tying your understanding to a certain implementation of a bridge just limits your ideas about what is, in fact, an abstract concept.

We understood functions as "mappings" between objects for hundreds of years, and when programming came along it gave us new ways to think about functions, but being able to "make" a function in hardware/software doesn't actually change what a function is at its core.

There's a reason why computer science professors explain concepts at a high or abstract level and don't jump into implementation to help students understand them. It's because the concepts ARE the high-level meanings, and if you need to see an implementation then you're not really understanding the idea for what it is.

If an idea stays abstract in your mind, it gives you more flexibility with how you apply it and more ways to find use out of it. But it does take a mindset shift, and is an actual learned skill, to be able to see something as purely abstract and accept that how it's made doesn't matter.

-- Edit - just realized who I was replying to. So take this comment as not meant to be lecturing, but just a 2c to offer :).


> if you need to see an implementation then you're not really understanding the idea for what it is.

I 100% disagree. At the very least, I think you're wrong if your assumption is that such a statement applies in general. That statement certainly doesn't fit me as well as many people I've taught in the past. I got my PhD in (pure) mathematics and I could only understand high level abstractions _after_ I worked through many concrete examples. I know the same applies for many successful mathematical researchers because we discussed the subject at length. Now such a statement certainly does apply for _some_ people (I've taught them as well), but certainly not all.

If you're someone that likes this sort of abstract thinking, that's great. If you're someone that needs concrete examples to understand, that's great too. The real lesson is that everyone learns differently.


But there's a difference between trying to understand, say, a theorem by applying it in concrete situations and by studying its proof.


> But there's a difference between trying to understand, say, a theorem by applying it in concrete situations and by studying its proof.

There may be a difference or there may not. You could study a proof within the context of a specific example. That is how I usually would do it. But yes of course it's possible not to do like that (many people don't study proofs that way). In any case, I don't really understand your point.


I was drawing an analogy, in this particular context, between studying the proof and taking something apart.


I didn't understand how engines worked until I took them apart, either. I was taking things apart to understand them long before computers :-)

But the notions of sending a "message" to a "method" just was way way too handwavy for me. I like examining the theory after I learn the nuts and bolts.

> Understanding what a bridge is doesn't mean knowing how to build one

If you don't know how to build one, you don't understand them. Architects who get commissions to design innovative skyscrapers definitely know how to build them.


Some months ago I had to learn React for a project. I was struggling a lot. The documentation was so poor. And everything looked so inconsistent (it still does...) and the "solutions" on stackoverflow looked so arbitrary. Until I remembered the lessons I had learned in the past and sat down and studied how it was internally working. Then things started to make sense.

I think it's an advantage to be able to mentally map high-level constructs to low-level operations. (Edit) Learning the low-level stuff first can help to understand what problems high-level languages are trying to solve. For example, the first languages that I learned were assembly and BASIC. Many people said that learning such low-level languages would make it harder for you to learn abstract thinking and structured programming with high-level languages. For me, it was quite the opposite. Writing complex programs in BASIC was so cumbersome, it made me appreciate programming in C with local variables and data structures. After mastering function pointers in C and discovering that you can elegantly express operations on "living" data structures with them (I wrote simple game engines and world simulations for fun), the concept of messages and methods in OO languages looked so natural when I first learned about them. And once you witnessed the mess of complex interdependencies in large programs (with or without multithreading), immutability and functional programming looked like the right way.


> Writing complex programs in BASIC was so cumbersome, it made me appreciate programming in C with local variables and data structures

My language trajectory was approximately from BASIC to Pascal to C to Common Lisp, but I had a very similar reaction. My move from C to Common Lisp probably had the greatest increase in appreciation because a task I semi-failed to complete in three years of C programming took me six months in CL, which was perfectly suited to the task in hand.

(the C task was postgraduate work in AI, in the late 1980s. As well as the importance of choosing the right language for the task, I also learned a lot about the importance of clearly agreed requirements, integration tests and software project management, none of which really existed on the four-person project I was working on).


On the other hand, you can walk over a bridge any amount of times without understanding the nuts and bolts of it. There are plenty of ways to build a bridge, there are static concrete bridges, there are bridges that can open, there are hanging bridges and a lot more variants. But for the people making use of them, the implementation matter a lot less than the purpose - connecting two places.

But yes, you are of course right in that if you build bridges, you need to understand the mechanics, and someone that builds a language of course need to have a deeper understanding of how the underlying abstractions work together than most of the users will have.


Thank you, what you wrote is exactly what I meant.


Is it possible to really learn how programming concepts work, though? Modern optimizing compiler are pretty amazing and just seeing the assembly output may make a concept harder to grasp.

To your bridge analogy. At this point what we are saying is "give me a method to traverse this river" and compilers are either building bridges, shooting out maps to fallen trees, or draining the river all together. If you looked at that output you might consider "traversing rivers" to only be walking over natural bridges.

This gets even more sticky when talking about types. Computers don't care about types, they are strictly a concept to help developers.

Or to your class point, would you know better what a class does if you pulled up godbolt and found that the this pointer is completely removed? You might come to the mistaken conclusion that classes are simply a name-spacing technique.


> Is it possible to really learn how programming concepts work, though? Modern optimizing compiler are pretty amazing and just seeing the assembly output may make a concept harder to grasp.

They usually have debugging options that let you read the internal steps (SIL, LLVM, GIMPLE, etc). That can be easier to understand than full asm, but also, the asm can't hide anything from you unless it's obfusticated.


> the asm can't hide anything from you unless it's obfusticated.

I think that misses the point. It's not the case of the ASM hiding things, it's a case of the optimizing compiler erasing things.

Here's a simple example: You say "Ok, the time I need to wait is 30 minutes. This method takes seconds, so I'll send in 60 (seconds in a minute) * 30 (minutes). The compiler is free to take that and say "Oh hey, 60 * 30? I know that's simply 1800 so I'll put that there instead of adding the instructions for multiplication".

Trying to learn how something like that works from the ASM output would leave you confused trying to reason where that "1800" came from. It's not hidden, it's not obfuscated. It's simply optimized.

That's a simple example, but certainly not the end of the optimizations a compiler can apply. The ASM isn't lying, but it also isn't telling the whole picture. The very nature of optimizing compilers is to erase parts of the picture that's ultimately only there for programmers benefit.


That's true, but often the things it erases are what's confusing you. For instance, it can remove highly abstracted C++ templates and dead code. Or if you don't know what a construct does, you can compile code with and without it and see exactly what it does.

Often new programmers think asm/low level programming is scary, but because it's so predictable it's actually quite easy to work with… in small doses.


> But the notions of sending a "message" to a "method" just was way way too handwavy for me. I like examining the theory after I learn the nuts and bolts.

Ooh-- are we talking about Smalltalk here? Java?

C'mon, Walter. You can't tell half a ghost story and then say goodnight. What did you find staring back at you deep in the void of all that object-orientedness?

Or, if this is a famous ghost story you've told before at least give us a link. :)


> sending a "message" to a "method"

I think you mean “to an object.”

The problem with an approach like yours is that implementations of abstract concepts often vary, and learning the “nuts and bolts” of one does not necessarily give you the true understanding of the concept itself.


Yeah, sending a message to an object via a method.


It’s because the “sending message” explanation is BS unless you talk languages like Erlang. The OOP explanations doesn’t make sense because they are BS. In the real world you don’t ask your shoe to tie itself, you don’t ask a tree to chop itself etc. And in OOP languages you normally don’t ask a string to send itself over a socket or draw itself in 3D using OpenGL. The reality is that you have code operating on data. The same way you have an axe operating on a tree, or your hands operating your shoe laces. That’s it. Everything else is BS.


>>"But the notions of sending a "message" to a "method" just was way way too handwavy for me."

This. I've felt like I haven't been able to keep up with commodity programming because I can't stand the way today's drivers (MSFT) control mindshare by emphasizing frameworks over mechanics. I feel like it's a people aggregation move instead of facilitating more creative solutions. The most enjoyable classes I had in uni were 370 assembler and all the Wirth language classes taught by the guy who ran the uni patent office.


> But the notions of sending a "message" to a "method"

You mean to an “object”?

Also, this looks like a perfect example of theory vs practice, having different implementations of object communication as in classic OOP vs actors etc?


Have you taken Rust appart yet?


it would have helped me a lot.from the outside it looks very complicated and capricious. I really do get the sense that the little axiomatic ur-language inside is a lot more tractable - would love to have learned that first.


same, i'd like to see such an approach


Chris, a friend of mine in college (who is unbelievably smart) decided one day to learn how to program. He read the FORTRAN-10 reference manual front to back, then wrote his first FORTRAN program. It ran correctly (as I said, the man is very smart) but ran unbelievably slowly.

Mystified, he asked another friend (Shal) to examine his code and tell him what he did wrong. Shal was amazed that his program worked the first time. But he instantly knew what was wrong with it - it wrote a file, character by character, by:

1. opening the file

2. appending a character

3. closing the file

Chris defended himself by saying he had no idea how I/O and disk systems worked, and so how could he know that the right way was:

1. open the file

2. write all the characters

3. close the file

and he was perfectly correct. This is why understanding only the abstractions does not work.


> This is why understanding only the abstractions does not work.

I don't think your example shows that at all: If it didn't actually explicitly say in his Fortran reference manual that "The 'thing' you write between a file-open and a file-close can only be a single character", then... Sorry, but then AFAICS your example only shows that he didn't understand the abstraction that "file-open" just opens a file for writing, without specifying what to write. (Maybe he slavishly followed some example in the manual that only wrote one character?)

This needless sprinkling of file-open / file-close looks a bit like he did the work of a current optimising compiler (only here it was pessimising), "unrolled a loop"... So AIUI it shows the opposite of what you set out to show: Too concrete without higher-level understanding was what did him in.


> I understand this thought process, but in my opinion it's the wrong way to think about software concepts. ... just limits your ideas about what is, in fact, an abstract concept.

There's nothing abstract about language constructs. Learning about a language construct via translation is a perfectly fine way of clarifying its semantics, whereas an "abstract" description can easily be too general and fail to pin down the concept; it can also rely on unstated "rules of the game" for how the abstract construct will operate within the program, that not everyone will understand the same way.


I tend to agree with you in principle, but for me too, a lot of high-level features are better understood in term of translations. Objects, coroutine, closures...

Even in formal CS, it's common to define the semantics of a language by translation, which can give more hindsight than operational semantics.

Now that I think of it, I think the problem is that most languages are defined informally which can be imprecise and inadequate.

The translation provided by the compiler is the closest thing we have to a formal semantics, it's natural to rely on it.


I've found that C compiler documentation on how their extensions work to be woefully inadequate. The only way to figure it out is to compile and examine the output. Most of the time, the implementors can't even be bothered to provide a grammar for their extensions.


> The translation provided by the compiler is the closest thing we have to a formal semantics, it's natural to rely on it.

Which translation, though? Depending on your compiler flags, you may get very different translations, sometimes (if your program contains undefined behavior) even with dramatically different runtime effects.


Yes, you are correct. But you are wrong too.

People need complete understanding of their tools. And complete understanding includes both how to use the concepts they represent and how those concepts map into real world objects. If you don't know both of those, you will be caught by surprise in a situation where you can't understand what is happening.

That focus on the high level only is the reason we had a generation of C++ developers that didn't understand the v-table while being perfectly capable of creating one by hand. It's also why we have framework-only developers that can't write the exact same code outside of the magical framework, even when it's adding no value at all.


IMO this is very elitist view of software developers' job.

The analogy from tangible world would be all the bridge engineers using "proven" / "boring" / "regulator endorsed" practices and techniques to build a "standard" bridge versus those constantly pushing the limits of materials and construction machines to build another World-Wonder-Bridge. There is nothing wrong with having both types of engineers.


> There is nothing wrong with having both types of engineers.

Acksherly, yes there is. In this context, there is: The world doesn't need engineers "constantly pushing the limits of materials" when building bridges; let's stick with proven, boring, regulator endorsed practices and techniques for that.


>There's a reason why computer science professors explain concepts at a high or abstract level and don't jump into implementation to help students understand them.

It's because they're trying to teach something to people who don't have anything to build on. Later in their education that'll be different. At the school I attended Object Oriented Programming class had Computer Organization as a pre-req and the teacher would often tangent into generated assembly to help students understand what was happening.

Regardless of whether I agree with your thoughts about the ideal approach to understanding the concepts vs implementation, I live in the status quo where - sooner or later - a C++ programmer is going to encounter a situation where they need to know what a vtable is.


I did not understand what virtual functions were at all until I examined the output of cfront.

Oh, it's just a table of function pointers.


You must have just had the bad luck to have read the most appallingly stupid books. I think you might be even a little older than I, and I got into the game late-ish; those C++ manuals or specs you read were probably from the 1970s or 80s? By the time I learned (imperative- and inheritance-based[1]) OOP from the Delphi manuals in the late nineties the virtual method table was explicitly mentioned and explained, along with stuff like "since all objects are allocated on the heap, all object references are implicitly pointers; therefore, Borland Object Pascal syntax omits the pointer dereferencing markers on object variable names" (which you also mention about C above). I'm fairly certain this gave the reader a pretty good grasp of how objects work in Delphi, but I still know nothing about what machine language the compiler generates for them.

It's not the idea of top-down learning from abstractions that's wrong, it's being given a shitty presentation -- more of an obfuscation, it seems -- to learn about the abstractions from that is the problem.

___

[1]: So yeah, that whole Smalltalk-ish "messaging paradigm" still feels like mumbo-jumbo to me... Perhaps because it's even older than C++, so there never were any sensible Borland manuals for it.


Sometimes textbook descriptions are just needlessly obtuse. I have noticed that there are some concepts which I already understood but if I were first introduced to them via the textbook, I would have been hopelessly confused. I wasn't confused only because I recognized this as something I already knew.


I agree that everyone shouldn't need to know every implementation detail, but I'd argue there should be more emphasis on the low-level details in CS education.

Programming is often approached from a purely abstract point of view, almost a branch of theoretical mathematics, but imho in 99% of cases it's better understood as the concrete task of programming a CPU to manipulate memory and hardware. That framing forces you to consider tradeoffs more carefully in terms of things like time and performance.

You shouldn't be able to hand translate every line of code you write into assembly, but in my experience you write much better code if you at least have an intuition about how something would compile.


I think that's the difference between computer science and programming. Yes, you'll be a better programmer if you focus on the low-level details, but you'll be a worse computer scientist.

I guess, if your goal is to write optimizations, focus on details. If your goal is to find solutions or think creatively, focus on abstractions. Obviously, there’s a lot of overlap, but I’m not sure how else to describe it.


I disagree that you'll be a worse computer scientist. Computer science doesn't happen in Plato's heaven, it happens in real processors and memory.

I tend to think focusing on abstractions is almost always the wrong approach. Abstractions should arise naturally as a solution to certain problems, like excess repetition, but you should almost always start as concrete as possible.


I would disagree with where you are drawing that line. I would say that computer science does happen in Plato's heaven; software engineering happens in real processors and memory. But most of us are actually software engineers (writing programs to do something) rather than computer scientists (writing programs to learn or teach something).


I agree that there is some value to purely theoretical work, but I think this is over-valued in CS. For instance, in the first year of physics instruction at university, problems are often stated in the form of: "In a frictionless environment..."

I think a lot of problems are created in the application of computer science because we treat reality as if there are no physical constraints - because often it is the case that our computers are powerful enough that we can safely ignore constraints - but in aggregate this approach leads to a lot of waste that we feel in every day life.

I think incremental cost should play a larger role in CS education, and if every practitioner thought about it more we would live in a better world.


> I think a lot of problems are created in the application of computer science because we treat reality as if there are no physical constraints - because often it is the case that our computers are powerful enough that we can safely ignore constraints - but in aggregate this approach leads to a lot of waste that we feel in every day life.

ObTangent: Bitcoin.


Undergrad "CS" education, probably for better (considering the career paths and demand), is more about teaching what you call software engineering than what you call computer science.


My company fired two Computer Science PhDs because they knew the theory brilliantly but couldn’t turn it into code. That’s the problem with only learning the theory.


This depends heavily on the context. In The Art of Computer Programming, the analysis of algorithms is done in terms of machine code. On the other hand, the proverbial centipede lost its ability to move as soon as it started wondering about how it moves.


I tend to think you should be able to go back and fourth between mental models. Like obviously when you're thinking through how to set up business logic, you should not be thinking in terms of registers and memory layouts.

But when you're considering two different architectural approaches, you should, at least in broad terms, be able to think about how the machine is going to execute that code and how the data is roughly going to be laid out in memory for instance.


The issue with viewing programming concepts as purely abstract is that the abstractions have varying degree of leakiness. Even with the most mathematically pure languages like Haskell you run into implementation details space leaks which you have to understand to build reliable systems.

There’s certainly something to be said for abstract understanding, but one thing I’ve learned in software and in business is that details matter, often in surprising ways.


> I understand this thought process, but in my opinion it's the wrong way to think about software concepts.

You cannot really tell someone that the way their brain learns is the "wrong way". Different people's brains are wired differently and thus need to learn in different ways.

> Understanding what a bridge is doesn't mean knowing how to build one, and in fact tying your understanding to a certain implementation of a bridge just limits your ideas about what is, in fact, an abstract concept.

Software development is about more than just understanding what something is. It's about understanding how to use it appropriately. For some people, simply being told "you use it this way" is enough. Other people like to understand the mechanics of the concept and can then deduce for themselves the correct way to use it.

I also fall into that category. While I'm not comparing my capabilities to Bright's I do very much need to understand how something is built to have confidence I understand how to use it correctly.

Neither approach is right nor wrong though, it's just differences in the way our brains are wired.

> There's a reason why computer science professors explain concepts at a high or abstract level and don't jump into implementation to help students understand them.

Professors need to appeal to the lowest common denominator so their approach naturally wouldn't be ideally suited for every student. It would be impossible to tailor classes to suit everyone's need perfectly without having highly individualised lessons and our current educational system is designed to function in that way.

> If an idea stays abstract in your mind, it gives you more flexibility with how you apply it and more ways to find use out of it. But it does take a mindset shift, and is an actual learned skill, to be able to see something as purely abstract and accept that how it's made doesn't matter.

The problem here is that abstract concepts might behave subtly differently in different implementations. So you still need to learn platform specifics when writing code on specific platforms, even if everyone took the high level abstract approach. Thus you're not actually gaining any more flexibility using either approach.

Also worth noting that your comment suggests that those of us who like to understand the construction cannot then derive an abstract afterwards. Clearly that's not going to be true. The only difference between your approach and Bright's is the journey you take to understand that abstract.


I like to understand the construction too, it's why I'm in engineering. I'm just saying it shouldn't be necessary in order to understand an idea. For me, it was a crutch I used for years before I grokked how to decouple interfaces from implementations because I naturally understand things better after I build them. But if you use an implementation to understand an idea, you couple implementation to interface in your mind, and so it changes your understanding.


> I'm just saying it shouldn't be necessary in order to understand an idea

Nobody said it is necessary. Some folk just said they find it easier learning this way.

> But if you use an implementation to understand an idea, you couple implementation to interface in your mind, and so it changes your understanding.

That's a risk either way. It's a risk that if you only learn the high level concept you miss the detail needed to use it correctly in a specific language too (OOP can differ quite significantly from one language to another). And it's also a risk that then when you learn that detail you might forget the high level abstract and still end up assuming the detail is universal. If we're getting worried about variables we cannot control then there's also a risk that you might just mishear the lecturer or read the source material incorrectly too. Heck, some days I'm just tired and nothing mentally sinks in regardless of how well it is explained.

There are an infinite ways you could teach something correctly and the student still misunderstand the topic. That's why practical exercises exist. Why course work exists. Why peer reviews exist. etc.

And just because you struggled to grasp abstract concepts one specific way it doesn't mean everyone else will struggle in that same way.


I am not a child psychologist, so take all of this with a grain of salt. I believe children first learn concepts by looking at and playing with concrete things first. "Oh look at this fun thing... Oh whoops I moved it, it looks slightly different, but if I rotate it, it looks like it used to... It doesn't really taste like anything, but it feels hard... Whoa, what's this new thing over here? Oh wait, this is the same size and shape as the thing I played with previously... In fact it behaves just like the first thing did. Oh cool, there's a whole stack of them over here, I bet they work just like the first things did!" This is how one might interpret a baby's first interactions with blocks. Later in life, they might find out about dice and understand some similarities. Later, still in school, the kid learns about cubes in geometry class, and can think back to all the concrete hands on experience he had and see how the various principles of cubes apply in real life.

So, people learn by experiencing concrete things first, and then grouping all those experiences into abstract concepts. Sometimes (ok, often) they'll group them incorrectly: Kid: "This thing doesn't have fur and moves without appendages. It's a snake. Whoa, look at this thing in the water, it moves without appendages either! It must also be a type of snake." Teacher: "That's an eel, not a snake." Kid: "oh. I guess snakes are for land and eels are for water" Teacher: "Water Moccasin is a type of snake that is quite adept in the water." Kid: "oh. They look kinda the same, what's the difference?" Teacher: [performs instruction]

This form of learning by compiling all sorts of concrete things down into a few abstract concepts is so powerful and automatic that we do it ALL THE TIME. It can even work against us, "overtraining" to use an ML term, like with our various biases, stereotypes, typecasting of actors ("this guy can only ever do comedies"). Sometimes folks need a little help in defining/refining abstract concepts, and that's the point that teachers will be most helpful.

So, for me anyway, and I suspect many others, the best way to learn a concept is to get many (as different as possible) concrete examples, maybe a few concrete "looks like the same thing but isn't", and THEN explain the abstract concept and its principles.

Or, to explain the process without words, look at Picasso's first drawing of a dog, and the progressively shinier simpler drawings until he gets to a dog drawn with a single curvy line.


I don't really buy this. It's like saying we should teach about fields before addition of real numbers, or about measure spaces before simply C^n. The most abstract version of a concept is usually much more difficult to grok.


You've never seen tutorials written that way because roughly nobody but you learns programming languages from the bottom up. There is just no demand.

By the way, where can I read a D tutorial from the bottom up?


I just added a -vasm switch to the dmd D compiler so you can learn it from the bottom up!

https://news.ycombinator.com/item?id=30058418

You're welcome!


Walter, watch out. You want to talk about dumbing down? California is considering a new law in which every computer language must have a keyword 'quine' which prints 'quine' . And none of this looking under the hood stuff. That's doing your own research. Trust the computer science! :)


Is there really no demand? Or do those of us who like to learn that way just get used to researching these things ourselves so quietly get on with it. Many of the existing tutorials are at least a good starting point to teach engineers what topics they need to examine in more detail.

Anecdotally, when I've mentored juniors engineers I've had no shortage of people ask me "why" when I've explain concepts at a high level; them preferring I start at the bottom and work my way up. So I quite believe there could be an untapped demand out there.


There’s a difference between understanding something and learning how and why it works the way it does. You can understand how a compilation pipeline works never working with any low-level code and never writing a compiler yourself. You can walk across a bridge and understand it connects point A with point B and don’t understand how a specific bridge has to be constructed. A concrete implementation is just an implementation detail and if you focus too much on it you’ll get tunnel-visioned instead of understanding the concept behind it

EDIT: And I say that as someone who likes both learning and teaching from the ground-up. But there’s no demand for it because that’s not how you efficiently learn the concepts and understand the basics so you can take a deeper dive yourself


> you’ll get tunnel-visioned instead of understanding the concept behind it

You might have gotten tunnel-visioned but it's not a problem I've suffered from when learning this way. And why do you think I cannot understand the concept behind the code after reading the code? If anything, I take the understanding I've grokked from that reference implementation and then compare it against other implementations. Compare usages. And then compare that back to the original docs. But usually I require reading the code before I understand what the docs are describing (maybe this is due my dyslexia?)

Remember when I said everyone's brain is wired differently? Well there's a lot of people today trying to tell me they understand how my brain works better than I understand it. Which is a little patronising tbh.


> You've never seen tutorials written that way because roughly nobody but you learns programming languages from the bottom up.

I am indeed a unique snowflake.


I also have a hard time with learning concepts too if there are handwavey parts of it. I remember by recreating the higher level concepts from lower level ones at times.


To me, the abstraction is an oversimplification of actual, physical, systemic processes. Show me the processes, and it's obvious what problem the abstraction solves. Show me only the abstraction, and you might as well have taught me a secret language you yourself invented to talk to an imaginary friend.


> abstraction is an oversimplification of actual

I think it’s an oversimplification of what abstraction is.


I don't believe most productive programmers learned the quantum physics required for representing and manipulating 1s and 0s before they learned out to program. Abstractions are useful and efficient.

You're more comfortable with a certain level of abstraction that's different from others. I can't endorse others that try to criticize your way of understanding the world, but I'd also prefer if some people who in this thread subscribe to this "bottom up" approach had a bit more humility.


I think part of it comes from believability, or the inability to make a mental model of what is going on under the hood. If something seems magical, you don't really understand what is going on, it can make it hard to work with because you can't predict it's behavior in a bunch of key scenarios. It basically comes down to what people are comfortable with what their axiom set is. It gets really bad when the axiom set is uneven when your teaching it, and some higher abstractions are treated as axiomatic / hand waved, while other higher abstractions are filled in. This is also probably an issue for the experienced, because they have some filled in abstractions that they bring from experience, so their understanding is uneven and the unevenness of their abstraction understanding bugs them.

Like limits in calculus involved infinity or dividing by unspecified number seems non functional or handwavy in itself. Like how the hell does that actually function in a finite world then? Why can't you actually specify the epsilon to be a concrete number, etc? If you hand wave over it, then using calculus just feels like magic spells and ritual, vs. actual understanding. The more that 'ritual' bugs you, the less your able to accept it and becomes a blocker. This can be an issue if you learned math as a finite thing that matches to reality for the most part.

For me to solve the calculus issue, I had to realize that math is basically an RPG game, and doesn't actually need to match reality with it's finite limits or deal with edge cases like phase changes that might pop up once you reach certain large number thresholds. It's a game and it totally, completely does not have to match actual reality. When I digged into this with my math professors, they told me real continuous math starts in a 3rd year analysis class and sorry about the current handwaving, and no, we wont make an alternative math degree path that starts with zero handwave and builds it up from the bottom.


The last time I learned a new programming language (Squirrel), I did so by reading the VM and compiler source code in detail rather than writing code. You get a far more complete picture of the semantics that way! I didn't even read much of the documentation first; it answered far too few of my questions. (Edit:) I want to know things such as: how much overhead do function calls have, what's the in-memory size of various data types, which strings are interned, can I switch coroutines from inside a C function called from Squirrel...


> I want to know things such as: how much overhead do function calls have, what's the in-memory size of various data types, which strings are interned, can I switch coroutines from inside a C function called from Squirrel...

So is that a problem with learning from abstractions, or just simply a problem that this stuff isn't mentioned in the manual?


I do. I recommend it as a way to avoid thinking Haskell is magic, which a lot of people seem to be convinced of. GHC has pretty good desugared printing options.

I'm not sure how to view asm for HotSpot or a JavaScript engine though.


I like my abstractions to be hidden, but I also like to be able to peek under the hood. That's one of the problems of C++ templates, sometimes I want to look at the expanded code.

The GNAT Ada compiler has an option to output a much-simplified desugared code. Not compilable Ada, but very inspectable unrolled, expanded code. Makes for a great teaching tool. 'Aaaaaaah this generic mechanism does that!'

Link https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ug... look up -gnatG[=nn]... Good stuff.


That's how I learnt C too. Couldn't grok how pointers worked. Took a few months to work with assembly. Returned. Didn't have to read any C tutorial. Everything came naturally


Fortunately, I picked up the K+R book after I was an experienced PDP-11 assembler programmer. I had never heard of C before, and basically just flipped through the pages and instantly got it. I quit all the other languages then, too, and switched to C.


To be fair though that's just an extremely well put together book. It's exceptional.

I happen to have the Second Editions of both Kernighan & Ritchie and of Stroustrup's much more long-winded book about his language sitting near this PC.

Even without looking at the actual material the indices give the game away. If I am wondering about a concept in K&R and can't instantly find it from memory, the index will take me straight there. Contrast Stroustrup where the index may lack an entry for an important topic (presumably it was in great part or entirely machine generated and the machine has no idea what a "topic" is, it's just matching words) or there may be a reference but it's for the wrong page (the perils of not-so-bright automatic index generation) and so the reader must laboriously do the index's job for it.

Now, today that's not such a big deal, I have electronic copies of reference works and so I have search but these aren't books from 2022, Stroustrup wrote his book in 1991 and the 2nd edition of K&R is a little older. This mattered when they were written, and when I first owned them. K&R is a much better book than almost any other on the topic.

The book, I would argue, actually holds up much better in 2022 than the language.


> the perils of not-so-bright

Fortunately, my parents defined me out of that category.


There are apparently whole books written about C pointers. It's definitely a topic where the best (?) way to teach it, in my view, is to sit there and just force a student to watch everything I do while I answer questions since you need a push over the activation energy to be able to work things out yourself.


> There are apparently whole books written about C pointers.

Spend 30 minutes teaching him assembly, and the pointer problem will vanish.


Unfortunately that's not quite how C pointers work - there are also things called pointer provenance, type aliasing, and out of bounds UB.


You’re very old school… I love it.

Edit: I just realized from the sister comment who I was replying to. Old school charm for sure. Now I love it even more.


FWIW, thanks a whole lot!

As an engineer in the forties it is somewhat encouraging to read that you felt the same way even if it was about different topics.

For me, these days it is about frontend generally, Gradle on backend and devops. So much to learn, so little documentation that makes sense for me. (I'm considered unusually useful in all projects I touch it seems but for me it is an uphill struggle each week.)

I always win in the end even if it means picking apart files, debugging huge stacks and a whole lot of reading docs and searching for docs, but why oh why can't even amazingly well funded projects make good documentation..?)


Just try, my pretties, just try to understand how exception handling actually works without staring at a lot of assembly.


Ah, doesn’t it just fly over to the nearest “catch”?

Btw, the worst misunderstandings I’ve seen were not lacking knowledge, they actively believed in some magic that isn’t there if you dig deeper. That’s why I still think that teaching at least basic assembler is necessary for professional programming. It can’t make you a low-level bare metal genius, but it clears many implicit misconceptions about how computers really work.


I recently picked up C after years of python, devops and javascript. I realized it's simply impossilbe for me to understand the tradeoffs made when other languages are designed or just understand my Unix-like operating system and other parts of it without knowing enough C. My next target is of course assembly and the compiler. And if anything I know, I want to stay away from any kind of sugar-syntax and unnecessary abstractions on top of basic computer and programming concepts.


Stack unrolling is a complicated process.

It's tempting to think of them as a kind of return value, but most languages do not represent them this way. (I believe it's a performance optimization.)

Flying to the nearest catch can also be complicated, as it's a block, that involves variable creation, and this possible stack changes. Again it's easier to model as a normal block break and then a jump, but that's not the usual implementation.


I always expect them to use (thread-) global state, not unlike good old errno just more structured. There's always at most one bubbling up so that's how I would do it.


But then you'll have a branch on every failable operation and slow down the happy path. This is not too different from passing the error as a value.

Instead, compilers use long jumps and manually edit the stack. I'm not sure it makes a lot of difference today, but branches were really problematic by the time the OOP languages were popularizing.


They're not bad these days if they're predictable. There's a code size cost, but there's also a cost to emitting all the cleanups and exception safety too.

For instance Swift doesn't have exceptions - it has try/catch but they simply return, though with a special ABI for error passing.


I believe that today a cold branch is just gets predicted as not taken and stays as such because it never jumps (in terms of a predictor’s statistics, not literally).


From the assembly one can learn what compilers do. But it cannot teach how modern CPU actually work. I.e. even with assembly reordering, branch prediction, register renames, cache interaction etc. are either hidden from code or exposed in a rather minimal way.


Right, in particular this is vital for Concurrency. In the 1990s my class about multi-tasking began by explaining that the computer can't really do more than one thing at a time. But in 2022 your computer almost certainly can do lots of things at the same time. And your puny human mind is likely not very well suited to properly understanding the consequences of that.

What's really going on is too much to incorporate into your day-to-day programming, and you'll want to live in the convenient fiction of Sequential Consistency almost all the time. But having some idea what's really going on behind that façade seems to me to be absolutely necessary if you care about performance characteristics.


Then there is no much difference between learning C and assembly. Both are languages for an abstract machine that has less and less relation to how things are done for real.


That is true.

But before I learned programming beyond BASIC, I took a course in solid state physics which went from semiconductors to transistors to nand gates to flip flops to adders.

Which made me very comfortable in understanding how CPUs worked. Not that I could design something with a billion transistors in it, but at the bottom it's still flip flops and adders.


> just fly over to the nearest “catch”?

No? (The stack unwinding is a whole process in and of itself.)


Well, I imagine that it would be possible by expressing its semantics using continuations. Implementing exception handling using call/cc seems like one of them favorite Scheme homeworks. And if you implement it that way, you should then know exactly what it does.


Although it's said that call/cc is a poor abstraction that's too powerful, and it'd be better to have its components instead.

https://okmij.org/ftp/continuations/against-callcc.html


Groovy? Gradle is a build tool for JVM.


Gradle.

Thankfully Groovy is not in the equation except for old Gradle files from before the Kotlin Gradle syntax existed.


A thousand times this! At the very least, I always want to have good mental model of how something probably works or mostly works even if I couldn't reproduce the implementation line-for-line. To me, it can almost be dangerous to have the power to use something without any idea of what's under the hood. You don't know the cost of using it, you don't have a good basis for knowing what the tradeoffs and reasons are for using it over something else, and the concept isn't portable if you need to work in a language or environment that doesn't have it.

If I come across a feature I like in a programming language, I usually find myself trying to figure out how to implement or emulate it in a language I already know (ideally one that won't hide too much from me). Implementing coroutines in C using switch statements, macros, and a little bit of state for example.


Since I've taken cars apart and put them back together, that has been very helpful to me in improving my driving skills. I also know when a problem with the car is just an annoyance and when I have to get it fixed. I can often know what to do to get the beast home again, without having to call a toe truck.


Absolutely! I have similar stories with other abstractions too (virtual methods, protocols, exceptions etc)

I just never quite understood where this "don't worry about the implementation" is coming from, as well as the tendency to explain abstractions in general terms, with analogies that make little sense, etc. The "don't worry about the implementation" did so much harm to humanity by producing bloated, wasteful software.

In fact I think a good abstraction is the one (1) whose implementation can be explained in clear terms, and (2) whose benefits are as clear once you know how it's implemented.


I'm similar. I've narrowed down my learning to two things I need:

- Principles for how things fit together. This is similar to your comment about digging into the assembly. Understanding how something is built is one way of determining the principles.

- Understanding of why something is needed. I still remember back to first learning to program and not understanding pointers. I pushed through in reading the book I was using and eventually they talked about how to use it and it finally clicked.


My dad has had trouble understanding classes for decades, but he had mostly stopped programming during that timeframe as well so it wasn't something he was going to put much time into learning. Now, he's returned to programming more regularly but still is having trouble with classes. I figured the big problem for him is exactly what the problem for you was, the hidden this pointer.

I'd started working on manually writing the same code is both C++ and C, but your approach of using something automated is an even better idea. Showing the implicit this pointer isn't hard to do manually, but polymorphism is a bit more of a pain. But I think the best part about using a tool is that he can change the C++ code and see how it affects the emitted C. Being able to tinker with inputs and see how they affect the output is huge when it comes to learning how something works.


Well, there is a difference between understanding "what it does" and "how it does what it does," and conflating the two is often a mistake. I have seen people take complex code apart (e.g. by doing manual macro-expansion), and it was not just a waste of time, it, in fact, hindered their understanding of the framework as a whole.

When learning Git, I enjoyed reading a tutorial that explained it from the bottom up, but, in the end, having been shown, early on, what is merely the implementation detail, created a cognitive noise that is now hard to get rid of.


I had a similar experience in law school in one of my tax classes. I hit a couple things where I could just not get what the tax code, the IRS regulations, or my textbook were trying to tell me.

I went to the university bookstore and found the textbook section for the university's undergraduate business degree programs, and bought the textbook for an accounting class.

Seeing the coverage of those tax areas from the accounting point of view cleared up what was going on, and then I understood what was going on in the code and regulations.


Learning how double-entry accounting works is both very simple and extremely useful for understanding any finance related topics.


I think like many programmers are "bottom up" (including me), I had a hard time understand virtual methods until I read an example on how they were implemented, then I was able to understand what it was and then the explanation why they were useful.

I remember two lessons about networks at school, the first one was "top to bottom" (layer 7 to layer 1), I understood nothing, then there was another one bottom up, and I finally understood networks..


>Anyhow, I essentially have a hard time learning how languages work without looking at the assembler coming out of the compiler. I learn languages from the bottom up, but I've never seen tutorials written that way.

Funny, that's similar to how I learned assembly! I wrote some small program, and then used lab equipment to monitor the bits flipping in the PC circuits...


> Years later, Java appears. I couldn't figure that out, either. I thought those variables were value types. Finally, I realized that they were reference types

I will resist replying to this.


Don't resist too hard, I don't want you to have an aneurysm!


this has been my approach to study also. some people are fine with what some might call "magic" and they never worry about lower level details. anyway if you want an approach from bottom up you should look at learning a lisp, especially common lisp

http://clhs.lisp.se/Body/f_disass.htm


When learning ReasonML it really helped to understand what Variants were by seeing the outputted JS code (being familiar with JS).


Yeah, I also always have the necessity to look what the compiler does with the code you through at it.


Sounds like you have a certain mental model of computation, and you can't understand other types of semantics. I suggest playing with a term rewriting language. If you can grok that without mapping it to your existing mental model, then other languages can be viewed through that lens much more easily.


> In video game design there is a saying: Show locked doors before you show a key

This is something I've tried putting into words many times.

I'll try to solve a problem and get to know its challenges deeply. Then a tool is introduced that brings it all together. In these cases, I seem to quickly get a full grasp of the operation and essence of the tool.

I wish education was based around this principle. A bit like what Paul Lockhart advocated for in "A Mathematician's Lament" (https://www.maa.org/external_archive/devlin/LockhartsLament....)


That's because the use case for classes appears when the information needed to understand the program exceeds the programmer's working memory. At some point, you need some way to make something into a black box whose innards you do not need to understand when not working inside the black box. Languages which do this badly do not scale well.

This is a hard problem. We have, at least, structs, classes, objects, traits, modules, and namespaces. There still isn't consensus on how best to do that.

At a higher level of grouping, we have more trouble. This is the level of DLLs, microservices, and remote APIs. We barely have language for talking about that. We have no word for that level of structure. I sometimes refer to that as the "big object" level. Attempts to formalize that level have led to nightmares such as CORBA, XML schemas, and JSON schemas, and a long history of mostly forgotten interface languages.

It's interesting to read that the author likes Python's new typing system. It seems kind of half-baked to have unenforced type declarations. Optional type declarations, sure, but unenforced ones?


> We barely have language for talking about that

The paper "The Power of Interoperability: Why Objects Are Inevitable" uses the term "Service Abstraction" - "This terminology emphasizes, following Kay, that objects are not primarily about representing and manipulating data, but are more about providing services in support of higher-level goals" It goes on to give examples of how service abstractions arise in a lot of places including the linux kernel.


Author here. I sure would prefer enforced type checking - but a good type checker that runs automatically in the editor and immediately warns me when I inevitably mess up is the next best thing.


> At a higher level of grouping, we have more trouble. This is the level of DLLs, microservices, and remote APIs. We barely have language for talking about that. We have no word for that level of structure. I sometimes refer to that as the "big object" level. Attempts to formalize that level have led to nightmares such as CORBA, XML schemas, and JSON schemas, and a long history of mostly forgotten interface languages.

Here I would like to think TLA+ would help. But it seems it doesn't either.


Python's static type system was kept out of the runtime object values to keep a slow language from getting slower.

Mypy is also not 1.0 yet.

You can still find bugs in advanced use cases i.e. when you are mixing inheritance, generics, data classes...

The new type system is both flawed and also a godsend for python developers.


I do not think JSON schema and XML schema are such nightmares though. They are not that difficult to write most of the time and are quite useful. Maybe they simply are not the best examples for what you want to express.


For the Python typing system. If you mostly code alone, you have the options to force yourself to enforce it, which would benefit the future you.


A modern IDE (I use PyCharm) also makes use of it for both warnings and autocompletion.


Yes, this is one of my pet peeves as well.

And what I think it's also important: don't ask for the key if the door doesn't need to be opened.

The examples on the text about Python and OOO are on point. Yes, people would reinvent classes if they needed, but the great thing about classes (and type annotations) are that they're optional

Compare with Java where you need a 'static void main' method inside a class just to begin. Why? Not even C++ requires that.

Do help them keep in their lanes but keep simple things simple.


The sheer irony is that "lite" object systems like python and ruby are actually purer and more faithful to the ideals of OOP (as envisioned by Alan Kay and those with him and before him) than Java can ever be, with its ugly and unnecessary seperation between classes and primitives, a completely irrelevant distraction that is primarily a VM optimization detail which only compilers and other bytecode producers or consumers should have known or cared about.

In python, nearly everything is a PyObject, data, code, source code files (which are just modules, which are just objects). It's highly misleading to name the things you get by calling a class objects, it implies this is somehow special, as if dicts and lists and ints, and classes themselves for crying out loud, aren't objects as well.

This is why I cringe so much when people associate OOP with Java bloat, the language is just unimaginably badly designed, this has nothing to do with OOP. You don't even have to be dynamic like Python and Ruby to be a good OOP system either, although that is a particularly low-friction path pioneered by smalltalk and lisp, but languages like Scala and Kotlin prove you can be highly static, highly OOP, and beautiful to read and write without the sheer amount of paperwork Java heaps on you.


It's not really an optimization detail, it's because Java doesn't have user-defined value types. The primitives are there because they're value types copied from C.

(Java is a memory-safe variant of Objective-C with all the interesting features removed.)


> Java where you need a 'static void main' method inside a class just to begin

C# too, but they remove the necessities to use static main for entrypoint, starting from C#9


In school, I would always go to the end of the chapter and look at the difficult exercises, think about them for a while and fail to solve them. This gave me the motivation to read the chapter and do the easier tasks. It puts your mind in "the right state", knowing that you are working on a real problem.

Later in life I've discovered a trick that gets me into this state of mind more easily. For any feature, I ask myself: What problem does it solve?


I was thinking about this recently when learning about Monads and that quote hits the nail on the head. It seems to me one of the main reasons so may blog posts try and fail to explain Monads is they try to explain "What" a monad is, without the motivations for why we need them. Unfortunately the "why" is very tied up the nature of functional style, laziness, etc. The primary use of a monad is it allows you to enforce execution order. If you're coming from an imperative language, execution order is a given so the reason you'd want monads really isn't clear.


But I think that's the exact reason we need to learn simple languages here. So that we know the closed doors first (the abstraction problem), then we will create a key or find a key elsewhere. Otherwise if "Class" is made the part of the language, like Java, you will not appreciate the importance of it to any extent and have a overall bad experience....


You can start with the simple features of a language and use them to introduce the more complex features.


Yep but in this case a language should be bundled with a good tutorial that introduces everything nicely. But if a person wants to just look at the specifications and code examples, the person would be having a lot of pointless keys..


> Then a tool is introduced that brings it all together.

I totally agree there and I think it's due to the fact that you've got a mental model of the problem space and your brain can see the empty hole. Once you have a tool of that shape, your brain knows where it goes and how it solves your problem.


This is very close to the image I have in my head. I visualize it as a puzzle: The more pieces you have, the more naturally more pieces come and the easier it becomes to see what you need and what is out of place.


hei, just read this. That's a really cool piece of writing.

It kinda put me in a mood of going over the basic principles of mathematics again, but in this new light.

Any recommendation of content that follows the advice given in the article?

Thanks!


Very quotable principle. Any attribution?


A succinct and level-headed assessment of the programming language landscape. Agree that beginners need to struggle with unwieldy personal projects before " getting" typing and classes.

"The idea is that you can't understand a solution without understanding the problem it solves," great quote!

"But mostly, software is bounded by its creation process: Programmers have limited time to create, and especially limited time to maintain, code." Software is this weird commodity with no marginal cost but some "complexity" cost.

Also, I remember taking a theory of the mind course and learned how people whose native language didn't have words for things like other people's minds or consciousness couldn't solve tests on those topics IIRC. Humans, in general, needed these words in our vocab to reason about these things. Just as adding words in our language helped us reason about higher-order concepts, these programming words and features allow us to reason about our code better. Furthermore, words in our language grow organically, just like these programming concepts.


> Also, I remember taking a theory of the mind course and learned how people whose native language didn't have words for things like other people's minds or consciousness couldn't solve tests on those topics IIRC.

Given that most animals can reason about other animals as agents with intentions, I'm not sure what people have been unable to answer. This also sounds a bit longer the discredited setting version of the Sappir-Whorf hypothesis (linguistic determinism).


> Humans, in general, needed these words in our vocab to reason about these things. Just as adding words in our language helped us reason about higher-order concepts

Except that words for concepts nobody's ever bothered to consider don't just appear in the language. That can only happen when somebody tries to think about the topic and is forced into making up some new words.


One difficult thing to accept in programming language design is that what mathematically is simple, consistent, and straightforward is not simple, consistent, or straightforward to people.

For example, a simple, consistent, and straightforward expression syntax would be RPN (Reverse Polish Notation). But humans dislike, and much prefer infix with its complicated operator precedences.

The real trick to programming language design is finding those constructs that are simple, consistent, and straightforward for users.


Just a quick word about polish notation:

The only reason why humans seem to prefer infix notation, is because it is what's being taught in school. The first mathematical expressions most people get to see is

    1 + 1
There is no reason why someone who was introduced to

    + 1 1
at an early age would find it less "natural" than infix. I think the only reason why infix is the dominant notation is historical: Prefix notation relies on getting whitespace right as a deliminator, and that is a lot tougher to do in handwriting. Infix solved this problem by (ab)using the operator as a deliminator.


That is not necessarily true. the first tells me we are starting with 1 apple and adding another one. the second tells me that we are starting with Adding? Adding what? We never start with an action without having a object/subject in mind.


You are still starting from your own learned context. Other languages than English have different word order conventions. As a simple extreme example, in classical Latin the main verb generally is placed last in the sentence.


White space is bad for readability anyways. I don't think that would be doable.


> White space is bad for readability anyways.

Question{Which|of|these|sentences|is|easier|to|read;This|one?}

Or this one in the next line?


I meant in the context of programming.


Ancient texts always seem to have no word breaks, they're just a solid wall of letters.


Some humans dislike RPN.

I am yet to find whether they really dislike or just find it unfamiliar.

I am convinced that unfamiliar gets conflated with unintuitive and hard all the time.


I remember back in the 70s in college the great debate was whether the HP-35 calculator with RPN was better or worse than the TI SR-50 with infix notation.

My conclusion was that RPN required less to remember than infix, and with a calculator you had to be very careful to not mess up where you were in punching buttons.

But for a tty, infix wins every time. Like no book publisher ever writes math equations with RPN, unless they are writing about Forth or Lithp or some intermediate code.

I should have known, however, that my remark about RPN not being human would flush out the 3 people for whom it is!


> unfamiliar gets conflated with unintuitive and hard all the time

In a deep sense unfamiliar and unintuitive/hard can be viewed as the same thing (see e.g. the invariance theorem for Kolgomorov Complexity). Hence striving to be "familiar" is still a virtue that it makes sense for a programming language to aspire to (balanced of course against other concerns).


Familiarity is a characteristic of the agent.

Intuitiveness/duficult is of the object.


Objects are not intrinsically difficult or intuitive though. Different people will perceive the same thing to have different levels of difficulty or intuitiveness.

Or as Von Neumann said about mathematics (and I think the same is applicable to CS and programming): "You don't understand things. You just get used to them."


'unfamiliar' is 'unintuitive'. You only have an intuition for things that are similar to things you have encountered. Some unfamiliar or unintuitive things might be simpler than familiar things. But, at first, it is often easier to use familiar things, even if they are less simple.

Regarding RPN, I am not convinced that they are actually simpler to a human. In order to understand an RPN expression I try to keep the stack of operands and intermediate results in my ultra short term memory. This can easily exceeds its typical capacity, whereas when trying to understand an infix expression, I can replace subtrees with an abstraction, i.e. a short name, in my mind. But perhaps I have just not yet seen the light and spend enough time working with RPN expressions.

On the other hand, RPN expressions are certainly far simpler to implement.


I never said PRN is simpler. It is just as hard as infix.

I do not like PNR any better or worse. It takes me about 5 minutes to switch from lisps to others and back. I just put the parens in the wrong place a couple of times and I am done.

Paredit + PNR makes editing slightly more comfy, but that's it. They are the same thing.


I think this is the takeaway as well; I prefer RPN (and I can read and I like k which is considered 'unreadable/readonly' and Lisp which is also apparently 'hard to read') because I am used to writing a lot of forth(likes); I like discovery while programming and it is quite trivial to stuff in a forth into anything, even when no-one did it before. For instance, 10 years ago, to speed up Xamarin (which was compile/run and quite slow at that), I sped up development by adding a Forth into C# so I could rapidly discover and prototype on my device without having to compile. And I have done that for the past 30 years, starting on my 80s homecomputer to avoid having to type hex codes and risk more crashing during discovering what I wanted to make.


I agree with your general principle but I do think infix notation is still better because so many equations are a tree of binary operators. Look at how binary trees are drawn - the nodes are in the middle of their children. It just makes sense for binary operators to have their operands on either side.

Otherwise you end up having to maintain some kind of mental stack which is just a bit mentally taxing.


There is something I am missing here.

How does your mental model change that much from where the operator goes?

Can't you put node to right of a column of children? Like you would do on a piecewise function.

I am dislexic, maybe that's why I do not see your point.


Prefix and postfix notations permit a great distance between the operands and the operators. If done in a disciplined fashion so that they're merely swapped (at least in the majority of cases), then sure, it's not too different. But consider this: (¬ used for unary negation)

  b ¬ b 2 ^ 4 a c * * - √ + 2 a * /
The two expressions being divided are quite large, so there's no easy way to just move the division one token to the right, it goes all the way to the end. Which two things are being divided at a glance, just point them out. I've even selected a more familiar expression for this exercise that should make it easier. But what about an expression people are less familiar with? Which thing are we calculating the square root of? This is what RPN looks like when entered into a calculator, which is handy and quick for computations (I mean, I have an HP-32S II at my desk next to my laptop, I like it). But even the HP-48G with its graphical equation editor didn't force you to think like this, algebraic notation provides locality of information that makes it much more comprehensible without overburdening your working memory.

And once you start adding parentheses or indentation to convey that information, you're on your way back to a tweaked infix notation. The same for prefix notations. If you can see past the parentheses of Lisp/Scheme it's not too hard to see what's being expressed (using the same symbols as above):

  (/ (+ (¬ b)
        (√ (- (^ b 2)
              (* 4 a c))))
     (* 2 a))
This is more comprehensible, but it's not a strict prefix notation anymore, we've used parentheses and indentation in order to convey information about the grouping. I've even borrowed Lisp's use of variable numbers of arguments to simplify *. If asked, "What is being divided by what", you can, rather quickly, identify the divisor and dividend, at least point them out if not express totally what they do. But a straight prefix notation or postfix notation makes that task very difficult.

You could start naming subexpressions, but then you get into one of the two hard problems in computer science (naming things).

And then we get to the other things people want to do with mathematical expressions, which is largely symbolic manipulation. The present infix notation (or a highly demarcated prefix/postfix notation like the Lisp-y one above) is much better for this task than a strict prefix/postfix notation.


Rust has this problem with its module system. It has top-level "items" that can be functions, types, global variables, …or modules. The way modules are defined and imported is consistent with defining and importing of named structs and functions.

But every new user is totally confused about this (I'm not even sure if my explanation above will be understood), because everyone expects modules to be a special case of imperative file loading operations, rather than declarative syntax creating named namespaced items, like everything else in the language.


D's original symbol table system was completely consistent, extensible, and orthogonal. I explained it till I was blue in the face, and exactly zero people understood it. D now has a significantly more complex one.


Rust has a mod declaration tree and a use import graph.


> But humans dislike, and much prefer infix with its complicated operator precedences

Depends on what you're writing; Gerald Jay Sussman claims that edge cases in mathematical notation (and corner-cutting of sorts in what is written) makes physics quite hard to grasp in "The role of programming" [1]; hence the Structure and Interpretation of Classical Mechanics book, which includes many Scheme programs which explicate everything.

[1] https://www.youtube.com/watch?v=arMH5GjBwUQ


> One difficult thing to accept in programming language design is that what mathematically is simple, consistent, and straightforward is not simple, consistent, or straightforward to people.

The problem here is that most 'people' have already learned the non-simple, non-consistend and bent logic of non-mathematical (aka imperative) programming languages. Because (almost) everybody has learned the basics of mathematics before programming, so for _everybody_ a term like `x = x + 1` had been a violation of all three.


> The complexity of Rust's ownership model is not added to an otherwise simple program, it is merely the compiler being extremely pedantic about your code obeying rules it had to obey anyway.

Not exactly. I remember when I learned Rust a few years ago (so maybe things are different today), sometimes the compiler didn't let me do things that should have been perfectly fine things to do. This forced me to write the code in a more complicated way than what should have been necessary.

I think this often happens beacuse the compiler's rules don't always match reality. Like if I have a struct S that contains members X and Y, the compiler doesn't let me have two mutable references to that struct even if one of them only changes X and the other one Y. In reality there is no problem but compiler thinks there is a problem so it forces me to write more complicated code.

I have had this almost exact problem in a program I was writing. The reason I could not just pass X to one function and Y to the other is that one of those functions I had to call was from a library that could only take an S. The solution was that I had to write a macro that extracted Y from S and call that macro every time I wanted to pass Y to a function.


> the compiler doesn't let me have two mutable references to that struct even if one of them only changes X and the other one Y.

The semantics of mutable references and other interface constructs must be independent from actual implementation, to allow for changes in the latter. So Rust is behaving as expected here. If the changes to X and Y are truly independent, you can have a function that returns separate mutable references to both ("splits" the struct). But if your library function takes an S, it might rely on the whole struct and then the changes are no longer truly independent.


> then the changes are no longer truly independent.

But the author of the code is telling you that they are independent. The author may be proved wrong if some future change accidentally triggers dependence but we as humans can have knowledge that the compiler can not have. The compiler sees a possibility of conflict and wants to protect you from yourself. That is a usability issue.


Rust is exploring techniques to allow "the author of the code" to provide finer "separation" semantics without resorting to unsafe - this is what GhostCell, QCell, LCell etc. are all about though their "usability" is still low. There's plenty of ongoing research in this space, so feel free to stay tuned for more!


Fun computer science fact: any type system will disallow some otherwise valid programs and also allow some invalid programs. The trick for type system designers is to find a sweet spot.


I'm curious if this is a provable result or an empirical observation?

(Come to think of it, I'm not even sure what's meant by program validity in this context.)


Programs are proofs and so this follows from Godel's incompleteness theorem. But "invalid program" needs defining.


> The solution was that I had to write a macro that extracted Y from S and call that macro every time I wanted to pass Y to a function.

Why does that require a macro?

``` struct S { x:X, y:Y }

fn do_with_y (y:&mut Y) {} fn do_with_s (y:&mut S) {}

fn main_program (mut s: S) { do_with_s(&mut s); do_with_y(&s.y); /* ... */ } ```

I prefer seeing code like this rather than wrapping it in abstractions and macros/helpers because it's not particularly verbose and very clear where the borrowing happens ; you'd see the same in C++ or C, to be honest.


I made a macro because it was more complicated than just S.Y. It was more like `Something.get_stuff().unwrap().things`


The definition of "simple" and "complex" is very vague here.

If we use Clojure's philosophy of simple, Python is by no means a simple language, even without classes. There are a lot of special syntax like `with` `as` `elif` `for .. else` and concepts such as `nonlocal`, not even mention generators, decorators and lots of fancy stuffs, although it is easy to learn...

I don't agree with the author that static type makes things more complicated... Instead type make things much simpler and easier to reason about.. In the Python's example, the author indeed like to reason about static typed systems...

And about classes, it is just a leaky abstraction legacy we have to deal with now.. "so they would have accidentally re-introduced classes" definitely no.. We can use structs, traits, interfaces or much better ways for abstraction than classes, which was introduced with ideas for inheritance.


Author here. Indeed, when I tested off my idea on a friend, his first response what "But what is complexity? Is J a complex language? What about assembly?". It can get tricky once you get into the weeds. But from the comments here, it looks like most people got what I was trying to express.

W.r.t whether types makes Python more complex - my point was that a type system makes the _language_ more complex, but it makes the _code_ using it easier to reason about. That's my main view of the blog post: Be careful trying to simplify a programming language if it leads to the code itself being more complex.

And sure, I have no particular attachment to OOP "classes" as such, and I don't think they are necessary. In fact I much prefer Julia's approach. What I meant with classes being inevitable is that 1) structs, or something like structs, are inevitable, and 2) methods, in the sense of functions that are semantically tried to particular structs, are inevitable. In Python, these two things are provided by classes, so in Python, there really is no escaping classes.


The big or small language problem have been debated quite a long time, for what I know especially in the scheme community. There are a lot of insightful discussions about R6RS, which you may find interesting.


> If we use Clojure's philosophy of simple, Python is by no means a simple language, even without classes. There are a lot of special syntax like `with` `as` `elif` `for .. else` and concepts such as `nonlocal`, not even mention generators, decorators and lots of fancy stuffs, although it is easy to learn...

Beware of the Turing tarpit where everything is simple but nothing is easy! Python is by no means perfect, but using different syntax when doing different things can make a lot of sense. "Everything is a X" can be a nice mental model, but on the other hand, as we've seen with classes, mixing data structures, structs and namespaces all in one concept is not always the best solution.


Yep, so as I said the definition of things like "simple", "complex" are super vague.

| but using different syntax when doing different things can make a lot of sense.

For C users yes, if we step back a little bit, for English users yes, but for people with other language background it may not hold. Every pieces of added syntax will utilise some human priors that may be specific to someone. But some really minimal systems can be grasped by every background, albeit taking a very long time.


My idea behind "make things that are different look different" doesn't come from language (at least not written language) but from visual media in general. For example, on a industrial machine or in a guide, people will often use lots of trick to distinguish some things from other. It isn't really syntax, but considering that programming is written text, we don't have much else than syntax to differentiate the parts on a quick read.

I think that may be a bias due to how I read code. I tend to skim it quickly at first, and then read the "more interesting parts" on a second pass. On the other hand, if you read code linearly, syntax may not be as needed. I'd like to see a study on eye movement when reading code and language preference, that may help reveal some patterns (or not.).


Static types are definitely make the language more complicated. It will have a bigger specification and more complicated implementation.

You could even argue it makes writing programs more complicated. But they definitely also make writing programs much much easier.

Kind of like how algebra is more complicated than basic arithmetic, but good luck proving Pythagoras's theorem without algebra.


What I meant is that the word "complicated" is vague. Even if you a using a language with dynamic type, it just hides the unavoidable complexity behind. All the problems with types will resurface at the run time. The language by itself allows very complex interactions with types, just that the user not specifying it. For me I would add the behaviour of the language as part of the language complexity, not the written part only.


>> good luck proving Pythagoras's theorem without algebra.

https://avatars.mds.yandex.net/get-zen_doc/48747/pub_5c9e493...


I don't understand. That uses algebra.


The proof is on the left, and doesn't require algebra. You can use another picture for the right part, like this https://microexcel.ru/wp-content/uploads/2020/05/kvadrat-sum...

(it also uses algebraic notation which can be ignored)


Pythagorus's theorem is literally written using algebra. You can't even write it down without algebra.


algebraic notation != algebra

It can totally be written without "algebra"; it's not the notation that contains the idea, it's only the idea that happens to be often expressed in this notation. I could use any other notation, or even plain English, to express the idea, if I didn't mind the extra verbosity.

For Example:

1. The Berlin Papyrus 6619 (from 2000-1786 BC Egypt) uses prose [1]

2. The Ancient Chinese mathematical text Zhuobi Suanjing uses both prose and a pictorial notation [2]

3. The Baudhāyana Shulbasūtra (from 800-500 BC), a set of mathematical instructions for use in the construction of Vedic fire-altars, uses Sanskrit prose describing geometric constructions using rope [3] [4].

[1] https://en.wikipedia.org/wiki/Berlin_Papyrus_6619#/Connectio...

[2] https://commons.wikimedia.org/wiki/File:Chinese_pythagoras.j...

[3] https://en.wikipedia.org/wiki/Baudhayana_sutras#Pythagorean_...

[4] https://en.wikipedia.org/wiki/Shulba_Sutras#Pythagorean_theo...


> algebraic notation != algebra

Ha where did you get that crazy idea?

> The Berlin Papyrus 6619 (from 2000-1786 BC Egypt) uses prose

Just because it's prose doesn't mean it isn't algebra. For example "If you square the lengths of the two shorter sides together and add them up it equals the length of the square of the length of the longer side". That's just algebra with words.

The thing that makes algebra algebra is that you use variables, not constants. "the length of the longer side" is just a wordy variable.


Google Einstein’s proof of the Pythagorean theorem. I’ve won quite a few bets with it. :-)


I did and they were full of algebra. Anyway as I've said elsewhere Pythagorus's theorem is an algebraic formula, so you can't even state it without using algebra.

Converting the equation to prose doesn't mean it isn't an algebra anymore. The key feature of algebra is manipulation of variables.


>Encapsulation, modularization, abstraction - let's not pick nits about the precise meaning of these terms, they are all about the same thing, really: Managing complexity of your own code.

The thing is, code complexity can be managed superbly without the concept of a class.

Golang doesn't have classes. C doesn't have classes. But both have structs and they have functions taking a pointer to, or a struct of type T. (what OO calls a "method" for some reason). So we can have "objects" as in, custom datatypes, and we can have functions to act on them.

And on top of that, we have modularization in libs and packages.

So, why do we need more complicated languages? "Show a locked door before you show a key" indeed.


> What more do we need?

Good languages with good IDE support?

I'm old enough that at some point assembly and C were my favourite languages and PHP was what I was most productive in and Python was also something I enjoy.

These days no way I go back for new projects.

After Java "clicked" for me and the introduction of Java generics shortly afterwards it became my favourite language and later TypeScript and C# had been added to that list.

I'm not to dumb, but I prefer using my smarts to develop programs, to work together or even pair program with the language to mold something, not to babysit languages that cannot even help me with trivial mistakes.


Is not the point, that a language with optional typing can help you out with trivial mistakes, if you give it sufficient information? Java makes (made?) you give all the info all the time, even when you do not need its help. Java holds your hand in an annoying way. TypeScript does not because you can gradually add types to your program, but is based on the shakier foundations of JS.


Yes, I would agree that structs are the solution to the complexity the author describes. But Python doesn't have structs: if you don't learn about classes you will probably eventually organize some data using dictionaries or heterogeneous tuples.

In another language you might introduce structs first (C and Go only have structs, C++ has both classes and structs but they are the same thing) but in Python you don't have that option - you can use data classes or named tuples but those already need most of the ceremony you introduce for dealing with classes.


If it’s useful: Python now has dataclasses, they are very struct-like.


> The thing is, code complexity can be managed superbly without the concept of a class.

It can be managed by modules/namespaces, but without even that (a la C), I really don’t see it managing complexity well. The important point of OOP is the visibility modifiers, not “methods on structs”, that would provide no added value whatsoever.

What OOP allows is - as mentioned - not having to worry about the underlying details at call site. Eg, you have a class with some strict invariant that must be upheld. In case of structs you have to be careful not to manually change anything/only do it through the appropriate function. And the bad thing is that you can make sure that your modification is correct as per the implementation, but what classes allow for is that this enforced invariant will remain so even when the class is modified - while now your call site usage may not fulfill the needed constraints.


> The important point of OOP is the visibility modifiers,

OOP is not required for implementation hiding. It can be done entirely by convention (eg. names starting with an underscore are internal and not to be used).

Go took this a step further and simply enforces a convention at compile time: Any symbol in a package not starting with an uppercase letter, is internal and cannot be accessed by the consumer.


Well yeah, and typing doesn’t need enforcement by compilers, we just have to manually check their usages. Maybe we should go back to Hungarian notation!

I mean, snark aside, I really don’t think that variable/field names should be overloaded with this functionality. But I have to agree that this functionality is indeed not much more than your mentioned convention.


OO implies far far more than simply grouping data in a struct and passing it by reference.


Such as?

The only other things that come to mind when I think about "OOP" are:

* inheritance, which at this point even lots of proponents of OOP have stopped defending

* encapsulation, which usually gets thrown out the window at some point anyway in sizeable codebases

* design patterns, which usually lead to loads of boilerplate code, and often hide implementation behind abstractions that serve little purpose other than to satisfy a design pattern...the best examples are the countless instances of "Dependency Injection" in situations where there is no choice of types to depend on.


How is encapsulation thrown out the window in sizeable codebases?? It is the single most important thing OOP gives, and is used by the majority of all programmers and has solid empirical evidence for its usefulness.


Could I see some examples of this evidence?

Let's consider a really simple object graph:

    A -> B
    C
A holds a reference to B, C has no reference to other objects. A is responsible for Bs state. By the principles of encapsulation, B is part of As state.

What if it turns out later, that C has business with B? It cannot pass a message to A, or B. So, we do this?

    A -> B <- C
Wait, no, we can't do that, because then B would be part of Cs state, and we violate the encapsulation. So, we have 2 options:

    1. C -> A -> B
    2. C <- X -> A -> B
Either we make C the god-object for A, or we introduce an abstract object X which holds references to A and C (but not to B, because, encapsulation). Both these implementations are problematic: in 1) A now becomes part of Cs state despite C having no business with A, and in 2) we introduce another entity in the codebase that serves no purpose other than as a mediator. And of course, A needs to be changed to accomodate passing the message through to B.

And now a new requirement comes along, and suddenly B needs to be able to pass a message back to C without a prior call from C. B has no reference to A, X or C (because then these would become part of its state). So now we need a mechanism for B to mutate its own state being observed by A, which then mutates its own state to relay the message up to X, which then passes a message to C.

And we haven't even started to talk about error handling yet.

Very quickly, such code becomes incredibly complex, so what often happens in the wild, is: People simply do this:

    A -> B <-> C
And at that point, there is no more encapsulation to speak of. B, and by extension B is part of A's state, and B is part of Cs state.


Encapsulation (to lock down an object like this) is used when there's a notion of coherence that makes sense. If B could be standalone, but is used by both A and C, then there's no reason for it to be "owned" by either A or C, only referenced by them (something else controls its lifecycle). Consider an HTTP server embedded in a more complex program. Where should it "live"?

If at the start of the program you decide that the server should be hidden like this:

  main
    ui
      http
    db
And main handles the mediation between db and ui (or they are aware of each other since they're at the same level), but later on you end up with something like this:

  main
    ui
      http
    db
    admin-ui
And the admin-ui has to push all interactions and receive all its interactions via the ui module, then it may make less sense (hypothetical and off the cuff so not a stellar example, I'll confess, but this is the concept). So you move the http portion up a level (or into yet-another-module so that access is still not totally unconstrained):

  main
    ui
    db
    admin-ui
    http
    -- or --
    web-interface
      http
Where `web-interface` or whatever it gets called offers a sufficiently constrained interface to make sense for your application. This movement happens in applications as they evolve over time, an insistence that once-written encapsulation is permanent is foolish. Examine the system, determine the nature of the interaction and relationship between components, and move them as necessary.

Arbitrary encapsulation is incoherent, I've seen plenty of programs that suffer from that. But that doesn't mean that encapsulation itself is an issue (something to be negotiated, but not a problem on its own).


Those are called callbacks, we use those everywhere. If you don't want to use callbacks, you can use something like a publish-subscribe pattern instead so that X doesn't need to be indirectly linked to B through A and can publish directly to X.


The one thing that is extremely hard to fake well without some sort of language support is multiple dispatch, the ability to call a method on an object x of type X and have the calling expression automatically "know" which method to invoke depending on the actual runtime type of the object (any subtype of X or any interfaces implemented by it etc etc etc...).

This is extremely hard to fake in, say, C. I mean it's not really that hard to fake the common use case demonstrated in classrooms, a rough sketch would be something like

enum vehicle_type {CAR, AIRPLANE} ;

Struct Vehicle {

enum vehicle_type t ;

Union {

struct car_t { <car-data-and-behavior>} car ;

struct airplane_t { <airplane-data-and-behavior> } airplane ;

} data ;

//some common data and behavior

} ;

void func (Vehicle *V) {

switch(V->t) {

   CAR :<....> break ;
   AIRPLANE :<....> break ;
} }

But this is a horrible thing to do. First, it's a repetitive pattern, you're basically working like a human compiler, you have a base template of C code that you're translating your ideas into. Rich source of bugs and confusions. Second, it's probably hiding tons and tons of bugs: are you really sure that every function that needs to switch on a Vehicle's type actually does that? correctly? what about the default case for the enum vehicle_type, which should never ever take any other value besides the valid two? how do you handle the "This Should NEVER Happen" case of it actually taking an invalid value? How do you force the handling of this in a consistent way across the code base that always exposes the problem to the calling code in a loud-yet-safe manner ?

There's a saying called Greenspun's tenth law : "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp". If you ignore the obnoxious lisp-worshiping at the surface, this aphorism is just trying to say the same thing as the article : there are patterns that appear over and over again in programming, so programming languages try to implement them once and for all, users can just say "I want dynamic polymorphism on Vehicle and its subtypes Car and Airplane" and voila, it's there, correctly and efficiently and invisibly implemented the first time with 0 effort on your part.

If you don't want the language to implement those patterns for you, you're still* going to need to implement them yourselves when you need them (and given how common they are, you will* need them sooner or later), only now haphazardly and repetitively and while in the middle of doing a thousand other thing unrelated to the feature, instead of as a clean set of generic tools that the language meticulously specify, implement and test years before you even write your code.

>inheritance, which at this point even lots of proponents of OOP have stopped defending

Not really, there are two things commonly called "inheritance" : implementation inheritance and interface inheritance. Implementation inheritance is inheriting all of the parent, both state and behaviour. It's not the work of the devil, you just need to be really careful with it, it represents a full is-a relationship, literally anything that makes sense on the parent must make sense on the child. Interface inheritance is a more relaxed version of this, it's just inheriting the behaviour of the parent. It represents a can-be-treated-as relationship.

Interface inheritance is widely and wildly used everywhere, everytime you use an interface in Go you're using inheritance. And it's not like implementation inheritance is a crime either, it's an extremely useful pattern that the Linux kernel, for instance, Greenspuns in C with quirky use of pointers. And it's still used in moderation in the languages that support it natively, its ignorant worshippers who advocate for building entire systems as meticulous 18th century taxonomies of types and subtypes just got bored and found some other trend to hype.

>encapsulation, which usually gets thrown out the window at some point anyway in sizeable codebases

I don't understand this, like at all. Encapsulation is having some state private to a class, only code inside the class can view it and (possibly) mutate it. Do you have a different definition than me? How, by the definition above, does Encapsulation not matter in sizable code? it's invented for large code.

>design patterns, which usually lead to loads of boilerplate code

100% Agreement here, OOP design patterns are some of the worst and most ignorant things the software industry ever stumbled upon. It reminds me of pseudoscience : "What's new isn't true, and what's true isn't new". The 'patterns' are either useful, but completely obvious and trivial, like the Singleton or the Proxy. Or they are non-obvious, but entirely ridiculous and convoluted, like the infamous Abstract Factory. This is without even getting into vague atrocities like MVC, which I swear that I can summon 10 conflicting definitions of with a single Google search.

The idea of a "design pattern" itself is completely normal and inevitable, the above C code for example is a design pattern for generically implementing a (poor man's version of) dynamic dispatch mechanism in a language that doesn't support one. A design pattern is any repetitive template for doing something that is not immediately obvious in the language, it's given a name for easy reference and recognition among the language community. It doesn't have anything to do with OOP or "clean code" or whatever nonsense clichés repeated mindlessly and performatively in technical job interviews. OOP hype in the 1990s and early 2000s poisoned the idea with so much falsehoods and ignorance that it's hard to discuss the reasonable version of it anymore. But that's not OOP problem, it's us faulty humans who see a good solution to a problem and run around like children trying to apply it to every other problem.


A language with a short learning curve is like a toolbox that’s nearly empty. You quickly run out of ways it could help you. We should optimize for experts, because that’s where each of us is going to spend most of his career.


Programming languages are not unique to programmers anymore, and not all users will ever become experts.

These days, programming is being thought to more pupils and students than ever, because languages like Python has made it very accessible and easy to learn.

The fact that a non-CS/IT person who has never written a code in their lives, can learn the basics with languages like Python, and make tools which increases productivity in just mere weeks, is amazing. I know this, because I've witnessed it multiple times at work.

That would almost certainly have never been the case, had we only had languages with steep learning curves. Imagine if someone wanted to make a web-scraper in C, and some program that act on the scraped data. In python, that's basically under 20 lines of code...in C? Most likely hundreds.


I fully agree with this. I think we're in the middle of a paradigm shift in regards to programming as more and more people enter the field or use programming as an auxiliary skill.

Like with writing/reading and math there was a time where only experts would do these things. There is still quite a substantial difference between a seasoned literary scholar and the average person, but many have access to this skill now and the fundamentals have become normal.

With programming I assume the same will happen. The fundamentals are simple and there are many ways, technological and cultural, to learn and use this skill.

Here's a list of programming types: Web development, systems programming, game engines, data analysis, application/UI scripting, scientific computing, spreadsheets...

I think we have to acknowledge and welcome this change and the people who will newly access this beautiful craft, while at the same time being mindful about what that means for professional identity and assumptions around that issue.


Exactly, I never how being able to learn a tool in a couple hours was something that made a programming language desirable to use. This is our profession, something we will be doing for a large period of our lives, choose the tool that results in the best output not the tool you can learn in an afternoon!


Is it possible to optimize too much for experts, and sacrifice learning curve too much?


Yes, as the creator of C++ has said [1] before.

Personally, I'd also distinguish between "complicated over time" and "complicated by default." For example, Clojure has a minimal syntax and instead uses a large vocabulary of terse primitive procedures. If you don't know the primitives, it's "complicated." A different example is Rust, which has many features expressed in syntax. If you haven't learned all of the syntax, it's "complicated." Compare these with C++, which began as a small set of extensions to a stronger-typed C, but has gradually grown into a family of sub-languages, each added for a particular purpose, and all interacting with each other (sometimes beautifully, other times horribly -- knowing the difference is "complicated").

[1]: https://blog.codinghorror.com/the-problem-with-c/


I don't think so, if you assume that learning is a linear thing and at the end of it you improve as a dev.

But the problem is that learning isn't one curve, it's a bunch of them and most of them lead to dead ends or even turn backwards after a while.

If you bloat out a language, what happens is that some subset of devs (like, maybe 10%) write really great code and have all the tools they'll ever need to do so. And the other 90% learn a chaotic mix of good and bad practices from each other, without the ability to properly distinguish them, and eventually reach some local maxima amongst themselves of overcomplicated "average" code that's shittier than I imagine it would be if they just kept the language simple.


C++ before 2011 springs to mind. From "Direction for ISO C++"[1]: "C++ is expert-friendly, but it cannot be just expert-friendly without losing important streams of new talent. This was the case before C++11 and C++11 reversed that negative trend."

[1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p200...


I do rather wish C++ 11 was a blank slate for that reason. Not really, as that'd kill C++, but there's so much good about C++ that is held back by its backwards compatibility with C and the dark ages of pre-11 C++. Data types and features are intended as best practice, but it's stuffed away in libraries since it can't replace the core of the language.


C++ evolution since Rust came along has been amusing. Finally, the C++ designers take safety seriously, now that there's a competitive threat. Until a decade ago, they didn't. I used to argue with some of the committee members over that.

The trouble is, C++ does safety by using templates to paper over the underlying C memory model. This never quite works. The mold always seeps through the wallpaper. Raw pointers are needed for too many APIs.


Languages don't get popular unless they are very easy to learn, so no language popular today almost surely is biased in favor of learning curve and against complexity. We probably need some big industrywide intervention to create the optimal level of complexity in languages as I doubt even C++ is too complex to be optimal if you consider that you work 40 years in a career. It would work by just having a couple of standard languages so developers don't need to learn so many languages, then they spend a lot of time learning those until they are fluent and then they don't have to learn anything new about programming languages for the rest of their lives, very efficient.


> It would work by just having a couple of standard languages so developers don't need to learn so many languages, then they spend a lot of time learning those until they are fluent and then they don't have to learn anything new about programming languages for the rest of their lives, very efficient.

Programming methodology continues to evolve and languages need to address it to stay relevant.


I certainly doubt any extant language actually does it, and also fundamentally there may just not be so much that's worth building into a general-purpose language. The most extreme examples I can think of are something like Haskell (which takes, what, a weekend to be able to write code in?) or maybe APL (which takes, what, maybe two months?). A language that was truly expert-oriented might take as long to learn as human languages do, or longer - certainly, for something you use your whole career, that much up-front effort would be worthwhile. But are there even enough distinct general-purpose constructs to fill that much time?


Try learning Rust, then you reach this point

https://stackoverflow.com/questions/39558633/higher-rank-lif...

I still don't understand what higher rank lifetime bound means and this affects whether I can write code that actually compiles


When I was writing my comment I thought about Scala, where I was certainly learning useful new techniques 7 years into using the language, but that wasn't really about learning new parts of the language. I think Rust has picked an overly specific way to represent lifetimes, that makes them more of a special case than they should be. But maybe that distinction doesn't actually matter.

(Higher rank lifetime bounds are like higher rank types, it might be easier to understand those first and then understand Rust's weird special case version of them afterwards)


Rust is in this weird place, because it needs to be palatable to C/C++ programmers. C has pointers and a vague notion of their lifetimes, but barely any type system to speak of. That's the lens through which Rust needs to present the problem.


There are certainly languages that test whether this is possible. J comes to mind: https://www.jsoftware.com/#/README

Here's something like the tenth or fifteenth chunk of code I wrote in J. It solves a programming puzzle. I doubt people who have mastered J would call it well-written, but it solves the problem.

  list=:_9]\33 30 10 _6 18 _7 _11 23 _6 16 _19 9 _26 _8 _19 _8 _21 _14 17 12 _14 31 _30 13 _13 19 16 _6 _11 1 17 _12 _4 _7 14 _21 18 _31 34 _22 17 _19 20 24 6 33 _18 17 _15 31 _5 3 27 _3 _18 _20 _18 31 6 4 _2 _12 24 27 14 4 _29 _3 5 _29 8 _12 _15 _7 _23 23 _9 _8 6 8 _12 33 _23 _19 _4 _8 _7 11 _12 31 _20 19 _15 _30 11 32 7 14 _5 _23 18 _32 _2 _31 _7 8 24 16 32 _4 _10 _14 _6 _1 0 23 23 25 0 _23 22 12 28 _27 15 4 _30 _13 _16 _3 _3 _32 _3 27 _31 22 1 26 4 _2 _13 26 17 14 _9 _18 3 _20 _27 _32 _11 27 13 _17 33 _7 19 _32 13 _31 _2 _24 _31 27 _31 _29 15 2 29 _15 33 _18 _23 15 28 0 30 _4 12 _32 _3 34 27 _25 _18 26 1 34 26 _21 _31 _10 _13 _30 _17 _12 _26 31 23 _31 _19 21 _17 _10 2 _23 23 _3 6 0 _3 _32 0 _10 _25 14 _19 9 14 _27 20 15 _5 _27 18 11 _6 24 7 _17 26 20 _31 _25 _25 4 _16 30 33 23 _4 _4 23
  sumOfRows =: ([: +/"1 [: |: ] \* [: |: _1 + 2 \* [: #: [: i. 2 ^ #)"1 list
  rowSigns =: (;/@:,/@:>)"2 {|:(_1;_1 1;1) {~ (_1&<+0&<) sumOfRows
  candidates =:,/((_4]\4#"3 (_1 + 2 \* #:i.2^9) (\* &. |:)" 1 _ list)  \* (>rowSigns))
  theAnswers =: (] #~ [: (0&<@:+/@:(0&<) \* \*/@:(_1&<)) [: |: +/"2) candidates
  theSums =: +/"1 +/"1 theAnswers
  theBestAnswer =: ~. ((theSums = ] theBestSum =: <./ theSums) # theAnswers)
In case anyone wants an explanation: https://gcanyon.wordpress.com/2009/10/30/a-solution-to-einav...


Haskell comes to mind.

In all seriousness, not really. Learning curve doesn't matter for a language that we'll pay your bills for the next twenty years. And once you fully internalize a JVM/.NET-level platform or Scala/C++-level language a lot of that will be reusable in learning others.


Yes, if nothing else because the problems that are solved shifts over time, as do preferred solutions. So if it is too optimized, there is a risk of churn.


If you can’t retain any or the project is short-term, I suppose. Probably nobody could learn a new language just for a hackathon.


Then they are quite bad developers, because that is exactly what a team of ours did in a two day hackacton.

Some learned C++ on the Arduino, some went with TypeScript and Angular, others with JavaScript and AWS Lambdas, together we got a robot arm to pick and drop pieces with a Web dashboard and remote control, talking with each other via the "cloud".

Naturally all of them had several years of coding experience, but in other languages.


It's definitely going to vary according to your familiarity with other languages in the family.

I came into a C# shop late last year from never touching C# before and was writing more or less idiomatic C# very quickly, but I've worked with lots of semi-colon languages so this is mostly familiar territory.

On the other hand if you've never seen a Lisp before, ten years of C++ and VisualBasic won't prepare you to get anything much done in Scheme. Your reflexes are all wrong, and that's going to take some unwinding before you're productive.

Also, people will hate it if you oblige them to use language A they don't know and which is poorly suited to the problem when they know language B that's well suited. Even if you've got a sane business rationale (e.g. bus factor, they're the only person who knows B in your organisation) they're going to spend a lot of time moaning about how terrible it is at this job they could do better.

I do not like C++ but I'm immediately dubious about whether I'd rather try to control a robot arm from Javascript. Is there an option where I just have my foot surgically removed by people who know what they're doing? I guess maybe if the robot manages most of this itself and I'm really only overseeing it the Javascript is less awful, but if there's an actual real time control loop I feel like I'm very much between a rock and a hard place.


It was an hackton, so all of that was throwaway code anyway.


> A language with a short learning curve is like a toolbox that’s nearly empty. You quickly run out of ways it could help you.

Very quotable!

That's why I like it when languages allow you to start writing simple code and gradually make it more complex. Haskell for example is not like that. Python does better here. I think Scala is one of the best languages in that regard.


> Haskell for example is not like that. Python does better here. I think Scala is one of the best languages in that regard.

This seems ripe for getting a lot of passionate anecdotes out of the woodwork.


Ha or not. Looks like I was wrong.


Not enough attention maybe? :)

Also, I like Haskell. It's just that the learning curve is much steeper at the beginning. This also has an advantage in that you will rather find experienced people in a project and don't have to deal with different styles within a project. The drawback is that it's harder to learn while being productive at the same time.


One trend with complicated languages is poor scalability although. They don’t compile fast , tend to have slow iteration speeds and don’t scale with large teams or codebases well. You see this with c++ , rust, haskell, scala and swift.


I'm curious to hear where you've seen Rust not scaling to large teams or codebases?


Generate 1 million lines of stupid repetitive code that varies slightly with a 100 line script and you'll see it happen quickly. Same issues with swift and even kotlin to some level. I don't have specific experience but I've run into it with other people who use both languages. My personal experience is swift.

If 1 million lines sound crazy to you, you just need 200 engineers working on one project for a year or two.


I'd also be interested in a Haskell example.


Generally this problem is because someone went crazy with template haskell or generics


What problem? I'm looking for an example of where poor scalability has been a problem in a Haskell codebase.


Learning curve has two dimensions, a short and steep learning curve can actually achieve a lot.

And that's why we call it a curve instead of "learning time" or "learning content", which are two different dimensions.


How does a short learning curve necessarily correlate to a "nearly empty" toolbox? That seems like a fallacy to me.

Ruby is fairly easy to learn. Does that mean it has a nearly empty toolbox? No it doesn't.

A language with a GC is easier to learn than one without a GC. Does the one with a GC have less in the toolbox? What if it also allows opting out of GC?

ObjC is considered a hard language to learn, esp for people used to C++ and Java. Does this come from it having more tools in the toolbox?

"A language with a short learning curve is like a toolbox that’s nearly empty" is a nice quote, but it also objectively seems to be wrong.


> Ruby is fairly easy to learn

No, its not. Ruby as a language is about as complex as they come.

It is easy to get to a productive state though, but that is not the same thing as having learned the language. Length of learning curve and accessibility are vastly different things.


Getting productive easily is the pretty much the definition of not having a steep learning curve. So I have difficulties making sense of your argument.


yxhuvud referred to length of the learning curve, not slope of the learning curve.

Even if getting started is easy (no steep slope), mastery can take long.


Python is the same. It's easy to write simple code, but once you get deep enough, the skeletons come out of closet.


I would assume that if you have a toolbox with only a small number of tools, you would spend some of your time using that set of tools to make other tools that extend your abilities.

I know that many developers in earlier times, before the internet, when programming languages were more limited and access to library repositories was not possible most experienced programmers built up their own libraries of functions that they would use and add to as required.

These days, we still have these extensions to the languages, but they are shared in repos and are called shared libraries or frameworks.


if you have a toolbox with only a small number of tools, you would spend some of your time using that set of tools to make other tools that extend your abilities

There is a big issue. We are much worse than that. We iterate through workshops (even the ones we built ourselves), and also our products grow out of them — they never leave a workshop they were done in. As a result, when you come to a new place, it itself is unlike anything in your previous workshops, and the product itself is unlike anything you could use your previous toolkits on. Doesn’t really matter how easy it is to bring all your toolboxes with you, unless it’s a new empty place. That’s why agreeing on a rich-from-the-start workshop is important.


I don’t disagree but there’s also needless confusion brought on by syntax like the C++ pure virtual function syntax (=0;) that encourages mental models that don’t map 1:1 with what’s really going on.


At the same time, a language with built-in abstractions that are not fitting the problem at hand will force its users to spend time working around the bad abstractions, which will be messier than not having the abstractions in the first place. More churn can definitely be expected in a complex language.


What do you have in mind? I think the language providing an abstraction doesn’t mean in most cases that it has to be used — if classes are not a good fit for this specific thing, just don’t use them. So that would still boil down to inexperienced developers not using proper abstractions - which can happen (with smaller blast radius) in less expressive languages as well.


The toolbox analogy breaks down when you realize that every language feature interacts with every other language feature. So it's more like a toolbelt that you have to wear. Hopefully, the tools aren't so plentyful and heavy that you spend all your energy dragging it along!


Most software is written and maintained by non-experts. Consequently you should optimize for time-to-onboard. Good tools are intuitive and easy to master; a hammer is basically better than a scanning electron microscope.


Are you talking about the languages themselves, or the languages plus their ecosystems (of libraries, design idioms, etc.)? Because if you're just talking about the PL itself, I'd really disagree


I think Excel shows us that you can do a lot with a beginner friendly toolbox.


Your point is well made, but in the case of Excel, the real question is should you do a lot with it?


Given their example about classes and "don't show the key before showing the lock", it's ironic the writer pans languages aiming to be "simpler" without understanding the kind of complexity they're actually avoiding. Zig, Go, (and Java and C# and other not as sexy and modern examples) are avoiding C++'s complexity - features with complicated interactions between each other that can sink your program without you being aware at all.

My favourite example is how the integer promotion rules make multiplication of unsigned shorts undefined behaviour, even though unsigned integral numbers have well-defined wrap-around arithmetic. Going by [1], we have the following rules:

1. Usual arithmetic conversions: The arguments of the following arithmetic operators undergo implicit conversions for the purpose of obtaining the common real type, which is the type in which the calculation is performed: (...) binary arithmetic: , /, %, +, -; (...) Both operands undergo integer promotions .

2. If int can represent the entire range of values of the original type (or the range of values of the original bit field), the value is converted to type int

3. (added after the above rules existed for the purposes of better optimizations) If the result of an arithmetic operation on two ints over- or underflows, that is Undefined Behaviour.

4. 0xFFFF 0xFFFF = 0xFFFE001 > INT_MAX

5. Therefore, two unsigned short values cannot be safely multiplied and must be explicitly cast to "unsigned int" each time.

[1] https://en.cppreference.com/w/c/language/conversion


The problem with implicit wrap-around of unsigned integer is that it makes your program slower. The underlying hardware with 2s complement does not need to pay the cost of reserving the overflow space or shifting/masking it out.

Further more, its more consistent if signed and unsigned behaving identical than having to look for the type with integer promotion rules of the number.

If you prefer the current C/C++ way of less performance and inconsistency that is fine.


And now I see that HackerNews ate my multiplication symbols. What I meant to type is that

    unsigned short a = 0xFFFF;
    unsigned short b = a;
    unsigned short c = a * b;
Is currently undefined behaviour on platforms where int has more bits than short (like x64 and arm) due to the interaction between the integer promotion rules and undefined integer overflow.


Rust here says OK, we'll define Mul (the * operator) for the same type (and for references to that type) so

  let a: u16 = 0xFFFF;

  let b: u16 = a;

  let c: u16 = a * b;
... is going to overflow, Rust would actually detect that because 0xFFFF is a constant, so, this says "Silently do overflowing arithmetic" and er, no, it doesn't compile. However if you achieved the same thing via a blackbox or I/O Rust doesn't know at compile time this will overflow, in a Debug build it'll panic, in a Release build it does the same thing as:

  let c: u16 = a.wrapping_mul(b);
Because the latter is not an overflow (it's defined to do this), you can write that even in the constant case, it's just 1, the multiplication evaporates and c = 1.

In C++ if you can insist on the evaluation at compile time there is no UB, so you get an compiler error like Rust.


I don't like Rust's approach, but it is better than C's. Rust should either commit to wraparound or make the default int type support arbitrary values.

In C, the problem isn't the silent wraparound, the problem is that when the compiler sees that expression, it will assume that the resulting value is less than INT_MAX, and optimise accordingly. The other insidious problem is that wraparound is defined for other unsigned arithmetic, so a programmer that hasn't had this explained to them, or read the standard very carefully, would quite easily assume that arithmetic on unsigned short values is just as safe as it is for unsigned char, int or long, which is not the case.


I understand why you don't like C's behaviour here.

> Rust should either commit to wraparound or make the default int type support arbitrary values.

Committing to wrapping arithmetic everywhere just loses the ability to flag mistakes. Rust has today Wrapped<u32> and so on for people who know they want wrapped arithmetic. I'd guess there's a bunch of Wrapped<u8> out there, some Wrapped<i8> and maybe some Wrapped<i16> but I doubt any large types see much practical use, because programmers rarely actually want wrapping arithmetic.

The mistakes are real, they are why (thanks to whoever told me about this) C++ UBSAN in LLVM actually flags unsigned overflow even though that's not actually Undefined Behaviour. Because you almost certainly weren't expecting your "file offset" variable to wrap back to zero after adding to it.

For performance reasons your other preference isn't likely in Rust either. Type inference is not going to let you say "I don't care" and have BigNums in the same code where wrapping is most dangerous.

We can and should teach programmers to write checked arithmetic where that's what they meant, and Rust supports that approach much better than say C++. Also the most serious place people get tripped up is Wrangling Untrusted File Formats and you should use WUFFS to do that Safely.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: