A Beginner's Perspective
Today I can hardly remember the Logo syntax and commands. I recall that it had all of the basic elements of a programming language: variables, loops, conditionals, procedures, etc. But having only barely become a teenager and considering that this was my first programming language, I didn't have a clue what programming was really about, or how Logo fit in to the world of programming languages. I didn't even know that world existed. I was fumbling around in the dark with an underpowered flashlight, finding interesting little trinkets to play with, but having no concept of the enormous playhouse I was wandering around in. It's as if the programming world was like this:
Fast forward a couple years. Now I'm a sophomore in high school, and I'm learning a new programming language called Pascal. That's a big change from Logo because Pascal is a compiled language instead of an interpreted language. I had no idea of this difference at the time. In fact, I didn't even realize that Logo and Pascal were related in any way. I thought of Logo as a toy program and Pascal as my first programming language. I didn't even consider the possibility that Logo and Pascal shared many programming constructs, but with different syntax. You see, my addled teenage brain couldn't conceptualize the overlap between these two languages. I was too concerned with learning the Pascal constructs to be bothered with relating it to another language that I thought was irrelevant, even though that comparison probably would have helped me understand Pascal and Logo much better. So my view of the world expanded a bit:
Now this picture is in need of a little explanation. I'm not claiming here that Pascal is a proper super-set of Logo, only that I relearned in Pascal all of the programming constructs that I had learned in Logo, plus some additional ones like variable types, arrays, and recursion. That is not to say that those constructs don't exist in Logo. I honestly don't know, because I wasn't exposed to those concepts until I learned Pascal. So this is a picture of what I learned in these languages and how I should have organized that knowledge, but didn't. As for the black background, that shows what I don't even know I don't know at this point.
Even from this severely restricted view of programming, I was enthralled with the possibilities that programming presented. I was solving math puzzles, experimenting with the limits of the computer's memory and processor, and creating games to play around with. Basically, I was having a great time messing around with computers and seeing what they could do, and my flashlight was getting stronger. I could see more of this language playhouse and interact with it in new and interesting ways. I still didn't have any real concept of how large the programming world was, but my view of that world was about to get a lot bigger.
Macros and Templates and Pointers, Oh My!
During my senior year of high school, I took a course on object-oriented programming in C++. In order to get to C++, the course went through C programming first. Both languages were mind-bending compared to Pascal. Learning C introduced pointers and dynamic memory allocation, both of which I struggled through but didn't fully understand until much later. It also introduced macros, a powerful feature for extending the language that can easily be abused if you're not careful.
C++ not only added object-oriented programming, an entirely new paradigm compared to the structural programming I had been doing before, but it also threw in operator-overloading, function overloading, and template meta-programming. The shift to object-oriented programming was a game-changer. It helps you think about program organization in an entirely new way that permanently affects how you do software design, even during the times when you fall back to structural programming. That is one of the great advantages of learning new programming languages. Every language is different in some way, either by having different features or implementing features differently than other languages, and learning those differences expands your mind and makes you a better programmer.
At this point, my view of the world looked more like this:
So in senior year of high school, I was pretty comfortable with the basic programming constructs of variables, conditionals, and control structures after being exposed to them multiple times in different languages, but the newer constructs of object-oriented programming and dynamic memory were much more muddled in my mind. That's okay because college would introduce another language to help me in this exploration of the world of programming - Java.
A Memory is an Awful Thing to Waste
As I entered the Electrical and Computer Engineering program at UW-Madison, Java was on its meteoric rise as The Next Programming Language so naturally, most of the computer science courses I took used it. Learning Java was easy, and since it was a completely object-oriented language, I couldn't help but solidify my understanding of that concept. As for dynamic memory, Java used garbage collection to manage dynamically allocated objects for you, whereas in C/C++ you have to keep track of all of that memory yourself.
When comparing dynamic memory management between these languages, C is the worst because you have to manage the raw memory all by yourself. C++ is better since you just have to keep track of the objects, and the runtime remembers the size of those objects for you. But Java's garbage collector keeps track of everything for you. There are some edge cases that you need to be careful of, but for the most part you can create new objects and let the garbage collector delete them when they're not being used anymore. Thinking about that difference in memory models between C, C++, and Java was what finally made dynamic memory fall into place for me.
Notice the contrast between these two concepts. On the one hand, object-oriented programming is fairly similar between C++ and Java. On the other hand, the memory models are drastically different between C, C++, and Java. In both cases, knowing more than one language was instrumental in my understanding of these complex concepts.
Java is also missing a number of language features that C++ has. Macros, templates, operator overloading, and bit manipulation operators are all gone. But, Java does have much better string support than C/C++ and a great concurrency model for multi-threaded programming. The more I learned about Java, the more I appreciated the similarities and differences between it and C/C++, and that was making me a better programmer with a bigger view of the world. Here's what it looked like to me now:
One curious thing I've noticed through this whole learning process is that at every step of the way, I was totally oblivious to the fact that there were other programming languages out there that had dramatically different features and concepts than the ones I already knew. Each new language was a surprise and an unexpected learning experience. That wasn't going to change for quite a long time, and it's fascinating looking back and seeing how naive I was, especially in light of all of the languages and concepts I learned between this picture of the world of programming and my realization that the black void was actually filled with an incredibly diverse ecosystem of languages.
What Goes on Under the Hood
Before I was aware of that language diversity, I learned a few very important things about the workings of microprocessors and languages that deepened my understanding of programming. The first was assembly language, the language on which all other programming languages are based. Assembly is actually a family of languages, with each one being unique to a particular microprocessor architecture. The assembly code can be translated directly into the machine code that runs on that particular microprocessor, and compared to most programming languages, it is obscure. All higher level language constructs are built up from these low-level instructions, like the way the same Lego blocks can be assembled into a multitude of different space vehicles.
Knowing how language constructs are built up from basic instructions greatly increases your understanding of how they work, what their best uses are, and what their performance impact is. Even more so, learning assembly teaches you about pointers because they are fundamental to assembly coding. You can't do anything effectively in assembly until you understand pointers backwards and forwards. I thought I had them pretty much figured out before learning assembly, but I was dead wrong. After going through the mental machinations of assembly coding with near constant pointer arithmetic puzzles to solve, pointers in C/C++ made a whole lot more sense, and my programming reached a new level.
Most programmers don't code in assembly these days, though. How do we get from C or Java to assembly? Why, compilers and interpreters, of course. Knowing how to write high-level language constructs in assembly code by hand is one thing; knowing how to parse a high-level language and generate the assembly code automatically is even better. The course I took on compilers opened my eyes to the real inner workings of high-level languages in a way that learning more languages never could. It gave me an incredible appreciation for how much work high-level languages do for you, and how micro-optimizing your code can not only be useless, but detrimental to performance. The compiler is performing all kinds of optimizations that you can't possibly keep in your head as you write code, so the best thing you can do is make your code as human understandable as possible. Then let go and let the compiler do its job.
On the other side of assembly language lives the actual hardware, but the hardware was most likely designed using a language as well - a hardware description language. The two most common HDLs are VHDL and Verilog, and I learned the latter while taking multiple courses in computer architecture during college. Understanding both Verilog and computer architecture also deepened my understanding of programming. Like compilers, the hardware in modern superscalar out-of-order processors is doing all kinds of optimizations on the code it's running. So even if you turn off all optimizations in the compiler, the hardware optimizations are still going to change the way your code runs. Micro-optimizing your code really is a fool's errand.
As for programming in Verilog, it is unlike any other programming language I know of because everything happens at once. Every line of code, every assignment, every operation gets evaluated every clock cycle, and for statements not controlled by a clock, they are evaluated every time their inputs change. Now that's parallel programming. Understanding how that all works, and how to manage that kind of complexity, definitely improves your programming skills in other languages.
After these two key experiences, my view of the world really changed: