Saterdag 28 Maart 2020

What I've Learned From Programming Languages

I just finished up learning about fourteen new programming languages, and while my head may still be spinning, I've been struck by one thing about learning new languages. Every single new language I learn teaches me something new and valuable about programming. Some languages reveal many new things because they happen to be the first language I've learned based on a new programming paradigm, and other languages may expose only one or two new ideas because they overlap quite a lot with languages I already know. Every language has shown me at least one new thing, though, so I thought I'd take a look back and pick out one thing learned from each language I've encountered. Some of these languages I've used extensively and others I've barely scratched the surface, so I may miss some great insights in the languages less well-known to me, but that's okay. There's still plenty to reflect on.

Word cloud of programming languages

Logo
Logo was my first programming language. Of all of the introductory programming concepts it taught me, the main thing I learned from Logo was how to give the computer instructions in order to accomplish a goal. What exactly did I need to type in to get that turtle to move the way I wanted it to? I had to be precise and not make any mistakes because the computer could only do exactly what it was told. When things went awry, I had no one or nothing to blame but myself.

QBasic
My programming journey really started with QBasic. As with Logo, I learned a ton of things from it because it was one of the first languages I learned, but since I'm going to pick only one thing, I'm going with fun. QBasic showed me that programming could be fun, and that I loved solving coding puzzles and writing basic arcade games in it. For me, QBasic was the gateway to the entire programming world and it was where the fun all began.

Pascal
Pascal was the first language I learned through study and coursework in high school. Of all the things I learned with Pascal, the one thing that stands out most in my memory is functions. Learning how variables and control structures worked came easily to me, but function declarations, definitions, and calls were the first programming abstraction that really stretched my mind. The fact that the argument names in the function call could be different than the parameter names in the function definition, as well as the rules surrounding function scoping all took time to full assimilate. It would not be the last time a programming concept would challenge my understanding, and every time it does, it inspires me to learn even more about programming.

C
The primary abstraction I learned in C was pointers, and with that comes memory allocation. Pointers are an extremely powerful, confusing, and dangerous tool. With them, you can write elegant, concise algorithms for all kinds of problems, and you'll come back to the code later and have no idea how it works. Most languages try to temper and wrap pointers in soft packaging so they can be used more easily and cause less damage. C leaves them raw and exposed so you can use them to their full potential, but you have to be careful to use them wisely or spend hours debugging segmentation faults (or worse).

C++
The first object-oriented language I learned was C++, so of course, the one thing I learned about was classes (and objects and methods and inheritance and polymorphism and encapsulation. This all counts as one thing, right? I won't even mention all of the other new things I learned in C++.) Object-oriented programming was a huge paradigm shift, and not just for me, but for all programmers. That shift came for me with C++, and it was an entirely new way to organize and structure programs. With OOP, programs could support more complexity with less code so we could all write bigger, buggier software. Yay!

Java
I'm trying to keep this list in roughly the order I learned languages, so Java is next. Java finally did away with manual memory management with the introduction of a garbage collector. I actually had to learn to not worry so much about memory, and that took some time after all the scars left by C and C++. Having the language and runtime handle memory allocation and deallocation was liberating and more than a little disconcerting. At the time computers were just getting enough memory to make this form of memory management possible, but now we don't even think twice about using garbage collected languages for most things. Ah, the luxury of 16GB of RAM.

MIPS Assembly
I was in college going down the technology stack while studying computer architecture, so I learned how to program in assembly language with MIPS. MIPS taught me many things, but let's focus on register allocation. All of those variables, arguments, parameters, addresses and constants have to be managed somewhere in the processor, and that place is the register file. Depending on the processor, you may have anywhere from 8 to 32 (or more) named registers to work with, and much of assembly programming is figuring out how to efficiently get all of the program values you need in and out of that register file to use it most efficiently. Programming in assembly dramatically increased my appreciation for compilers.

Verilog HDL
Even further down the technology stack from assembly language is the physical digital gates of the processor, made up of transistors. It turns out that there are a couple programming languages that describe them, and they are aptly named hardware description languages. Verilog is the one I learned first (VHDL is largely the same, just three times more verbose), and it taught me about fine-grained, massively parallel programming. It's the ultimate concurrent programming language because everything, and I mean everything, in a Verilog program happens at once. Every bit of every variable moves through its combinational logic at the same time. The only way to manage this colossal network of signals is with a clock and flip-flops to create a state of the machine that changes over synchronized time periods. It's an entirely different way to program, and it's programming how you want the hardware of a digital circuit to behave.

SKILL
SKILL was the first scripting language I learned, and it was a proprietary language embedded in the super-expensive semiconductor design software called Cadence. Little did I know at the time, but SKILL is also a Lisp dialect. I did not learn much about functional programming with SKILL because it allowed parentheses to be used like this: append(list1 list2) and statements could be delimited with semicolons. However, it still had car, cdr, and cons, and there were still plenty of parentheses. What I learned without realizing it was how to program using lists as the main data (and code) abstraction. It was a powerful way to extend the functionality of Cadence, and to program in general.

MATLAB
My first mathematical programming was done with MATLAB. The one thing above all else that I learned in MATLAB was how to program with matrices to do linear algebra, statistical analysis, and digital signal processing in code. The abstractions provided for doing this kind of computing were powerful and made solving these kinds of problems easy and elegant. (To be clear, the matrix code was elegant. The rest of it, not so much.)

LabView
Yes, I'm not ashamed to say I learned a graphical programming language. Well, maybe a little ashamed. LabView taught me that for certain kinds of programming problems, laying the program out like a circuit can actually be a reasonably clear and understandable way to solve the problem. These types of problems mostly involve interfacing with a lot of external hardware that generates signals that would make sense to lay out in a schematic. LabView also taught me that you can never get a monitor big enough to effectively program in LabView.

Objective-C
I explored iOS programming for a short time around iOS 4, and the thing that I really loved about Objective-C was the named parameters in functions. While it may seem to make function calls unnecessarily verbose, naming the parameters eliminates a lot of confusion and allows literals to be used much more often without sacrificing readability. It turns out to be a pleasantly descriptive way to write functions, and I found that it made code much more clear and understandable.

Ruby
There is so much to love about Ruby, but I'm limiting myself to one thing so for this exercise I'm going to go with code blocks. Being able to wrap up snippets of code to pass into functions so that it can be called by the function as needed is an incredibly awesome abstraction. Plus code blocks are closures, and that just increases their usefulness. I think code blocks are one of the most elegant and beautiful programming abstractions I've learned, and I still remember the giddy feeling I got the first time I grokked them.

JavaScript
JavaScript is the first prototypical language I learned. Programming with objects, but not classes, was a shocking experience. After spending so much time in OOP land, learning how to create objects from other objects and then change their parameters to suite the needs of the problem can be a powerful programming paradigm. There aren't that many different programming paradigms, so every chance to learn a new one is a valuable experience. It teaches you so much about entirely new ways to solve problems and organize your code.

CoffeeScript
I learned CoffeeScript in tandem with Ruby on Rails, since it's the default language used for coding front-end interfaces. With CoffeeScript, I learned about transpiling—the act of compiling one programming language into another instead of an assembly language. CoffeeScript is neither interpreted nor compiled into an assembly language, but instead it's compiled into JavaScript to run in the browser (or Node.js). Learning that you don't have to live with JavaScript's warts if you would rather transpile from another language was certainly eye opening, and pleasantly so.

Python
List comprehensions are to Python what code blocks are to Ruby. It's amazing how much you can express in a simple one line list comprehension. You can map. You can filter. You can process files. You can do much more than that, too. List comprehensions are an excellent feature, and they make Python programming exciting and fun. They are a scalpel, though, and not a chain saw. The best list comprehensions are neat little cuts in code. If you try to make one big one do everything you need to a list, it's going to get complicated and confusing real fast. List comprehensions are a precision tool that solves many kinds of little problems very well.

C#
When Microsoft designed C#, they tried to put everything in it. Then they kept adding more with each release of the language. Somehow through it all, they managed to make a fairly decent and usable language with some nice features. One of the best features that was new to me is delegates. A delegate is basically a special-purpose list of function pointers. Classes can add one of their functions to the delegate of another class. Then the class that has the delegate can call all of the functions attached to the delegate when certain things happen, like when a button in a UI is clicked, for example. This abstraction turns out to be exactly what's needed to make GUI programming elegant and clean. It not only works for the UI, but for all of the different asynchronous events happening in a GUI program.

Scheme
My first experience with a functional language that I knew was a functional language was with Scheme. Learning how powerful functional programming can be was another eye-opening experience, as learning any new programming paradigm can be. Part of what makes functional programming so expressive is how natural it is to use recursion to solve problems. Recursion is used in Scheme like pointers are used in C. It's the other fundamental programming abstraction.

Io
We have finally gotten to the latest languages I learned in quick succession with the 7 in 7 books. Io is the second prototypical language I've encountered, and while it is much simpler than JavaScript, it seems much more flexible as well. I learned that pretty much the behavior of the entire language is controlled by the functions in certain slots of an object, and these slots can be changed to completely change the behavior of the language, add new syntax, and create custom DSLs on the fly. It's incredibly powerful and, well, scary. The ability to change so much about a language while your program is running is fascinating.

Prolog
So far we have procedural, object-oriented, prototypical, and functional programming paradigms. Prolog introduces another one: logic programming. In logic programming you don't so much write a program to solve a problem as you write facts and rules that describe the problem and have the computer search for the solution. It's an entirely different way to program, and when this paradigm fits the problem well, it feels like magic. The fundamental abstraction that you learn in logic programming is pattern matching, where you write rules that include variables and both sides of each rule need to match up. There is no assignment in the normal sense, though, and one program can generate multiple solutions because multiple combinations of values are attempted for the set of variables. Prolog will definitely change the way you think about programming.

Scala
The one thing to learn with Scala is concurrency done well with actors and messages. Actors can be defined with message loops to receive and process messages, and they can be spawned as separate threads. Then, threads can send messages to these actors to do work concurrently. The actor model of concurrency is a lot safer than many of the models that came before it, and it will help programmers develop much more stable concurrent programs to utilize all of those extra cores we now have available in modern processors.

Erlang
If Scala taught me something about concurrency, Erlang taught me how to take concurrency to the extreme. With Erlang, concurrency is the normal way of doing things, and instead of threads, it uses lightweight processes. These processes are so easy to start up, and the VM is so rock solid, that it's actually easier to let processes fail when they encounter an error and start a new one instead. The mantra with Erlang is actually "Let it crash," which was simply shocking to me. After learning for decades to write careful error-checking code, seeing code with sparse error checking that instead monitored processes and restarted them if they failed was a big change. It's a fascinating concept.

Clojure
One of the Clojure features that stood out to me was lazy evaluation. With the addition of a number of constructs, Clojure is able to delay computation on sequences (a.k.a lists) until the result of the computation is needed. These lazy sequences can significantly improve performance for code that would otherwise compute huge lists of values that may not all be used in subsequent calculations. Generators can also be easily set up that will feed values to a consumer as needed without having to precompute them or specify an end to the sequence of values. It's a great trick to keep in mind for optimizing algorithms.

Haskell
The big take-away with Haskell is, of course, the type system. It's the most precise, well-done type system I've ever seen, yet it's not as onerous as it sounds because it leverages type inference. I was worried when starting out with Haskell that I would constantly be fighting the type system, but that's not at all the case. It still takes some time to design the types of a program properly, but once that's done it provides great structure and support for the rest of the program. I especially learned from this experience to not write off a certain way of programming just because I've gotten burned by it in the past with other languages. One language's weakness can be another language's strength, and a perceived weakness may not be inherent to the feature itself.

Lua
With Lua I learned that if one fundamental abstraction is made powerful enough, you can make the language be whatever you want it to be. Lua's tables provide that abstraction, and depending on how you use them, you can program as if Lua is an OO language, a prototypical language, or a functional language. You can also ignore all of that and use it as a procedural language if you so choose. The tables allow for all of these options and leave the power in the hands of the programmer.

Factor
The final programming paradigm I've learned happened through Factor, and it's stack-based programming, also known as concatenative programming because of the post-fix style of the code. Because of the global stack on which all values are pushed and popped (well, almost all), the stack needs to be constantly kept in mind when analyzing the code. This concept is mind-bending. I'm sure it gets better with practice, but for how long I've used the language, it's a significant mental effort to keep program execution straight. However, some problems can be solved so neatly with stacks that it's definitely worth keeping this paradigm in mind. Stacks can be used in any language!

Elm
Elm taught me another way to do UI development with signals. While C# introduced me to delegates with their PubSub model, signals are a finer-grained, more tightly integrated feature that makes front-end browser development so much cleaner. Signals can be set up to change state, to kick off tasks or other processing, or to chain different actions together when specified events happen. Signals take delegates to the next level, and provide a lot of functionality that you'd have to build yourself with delegates.

Elixir
Elixir combines a lot of things from a lot of other languages, but one thing not covered yet that I learned in Elixir is metaprogramming with macros. Macros are an extremely powerful tool for compacting programs and making difficult or tedious problems much simpler. Any time you're repeating similar code over and over, it's best to start thinking of how to implement it more quickly and efficiently using a macro. Spending any amount of time copy-pasting code and making slight changes is a dead giveaway that a macro could be used instead. Metaprogramming has a way of wringing the drudgery out of programming. It's simply magical.

Julia
The killer feature that I learned in Julia is definitely multiple dispatch. While other languages have to deal with calling the same function with different types in various convoluted ways, Julia says "Screw it. Just give all of those functions the same name with different types, and I'll figure it out." It's a great feature when you need it, and since Julia is built for scientific computing, it probably comes up a lot.

miniKanren
I've learned about how great logic programming can be for certain types of problems, and I've learned how great functional programming can be for other types of problems. Now miniKanren has shown me how powerful it is to combine these two paradigms into one language. Since logic programming works well for isolated logic problems, but doesn't do so well for implementing a complete program, putting it into a functional language bridges that gap so that you can create a fully working program. Having Clojure there to provide the input and process the output of the logic program really ups the ante for what can be accomplished in miniKanren.

Idris
Idris takes the powerful type system of Haskell and turns it up a notch. The novelty of Idris' type system is dependent types that allow types to be specified in relation to other types using characteristics of those other types and operations. It's basically making the type system programmable. Types can be specified so that arrays have a certain length, the output of a function has the same length or combination of lengths of its inputs, or any other set of operations can be done on characteristics of a function's types to specify the dependent type. It is a really powerful type system that stretched my mind even more than Haskell did.


As you can see, I've come in contact with a lot of languages. It looks like 31 so far, if I haven't forgotten any, and I've learned something valuable from each one of them. Some languages have taught me much more than others simply because of where they fell on my programming journey, but old or new, popular or unknown, every language has something to teach. While this is a lot of languages, there are notable omissions including Go, Rust, Swift, and Kotlin, just off the top of my head. I imagine that those languages, too, would have much to teach me about new programming features or improvements to concepts I already know. That's the beauty of learning new programming languages: there's always something more to learn about the craft. You just have to be ready for it.

Geen opmerkings nie:

Plaas 'n opmerking