Pick your battles: Choosing and Learning the “right” Programming Language
Finding differences and similarities as well as learning tips and tricks for picking up a programming language
Whether it be at work (where you may be working as a Software Engineer) or as a hobby, there comes a time when one struggles to choose the “right” programming language.
While there isn’t a fast and easy answer to this (and certainly, not “one language trumps all”), there are a few key metrics that we can investigate that can help us in this reflection. Along the way, we’ll also walk through concepts that span across programming languages, as well as some ways you can find yourself learning a programming language more quickly than you would think - whether you’re just interested in what programming is or a seasoned veteran who has been programming for years.
- Levels of abstraction
It can be helpful to look at today’s programming languages across 3 different levels — “build fast” programming languages used for prototyping applications as quickly as possible, “infrastructure” programming languages which help with performance-centric/use-case specific portions of your application and finally, “systems” programming languages useful for embedded hardware and use cases where you need absolute control of memory usage. Of course, lines along such levels tend to be a bit more blurred and muddy in reality, but it helps us form a mental map of how to approach problems! Recommendation: if you’re just starting out with developing applications, “Build fast” programming languages are highly recommended. They get you to see the end result of whatever you want to build, faster.
“Infrastructure” programming languages — you can lump JVM-based languages (Java, Kotlin, Scala, Clojure, etc) as well as binary-producing languages like GoLang, Swift and Haskell into this camp. These languages tend to target performance in exchange for conveniences (strong typing, less components out of the box from the standard library, “heavier” verbosity, etc) without losing too much as well as impose rules to make getting things wrong, harder. They tend to be more manually tunable based on your execution environment (by allowing you to pass arguments that can affect runtime code performance). If you have a piece of your application that needs performance boost — especially if it’s running on the web, consider one of these for extra mileage.
“Systems” programming languages — you can lump C, C++ and Rust into this camp. These languages provide you the most control for your application and explicit memory handling when need be. They also tend to work well with embedded devices (programmable micro controllers, computers with non-standard processor architectures) and hardware without much software support (e.g. accessing your car’s information through its OBD port). With the rise of WebAssembly, such “low-level” languages also prove useful for performing computation-intensive work in support of web applications.
- Features and maturity
Syntax and data structures — languages serve as communication tools between a computer and a programmer. Take advantage of the language’s syntax. Know the most frequently used data structures in a language as well as their underlying implementation’s time complexities for insertions/deletions/modifications.
Runtime environment — be familiar with how your application “works” with respect to your computer. Does it need a language interpreter (the likes of Python, NodeJS, PHP)? Does it produce an architecture-specific binary (the likes of Swift and GoLang)? Does it use a mix of both — compiled and ran on some virtual machine (the likes of Java, Scala, Clojure)? Due to needs like these, learning and using Docker is highly recommended as well — you’ll learn a bunch about Linux administration along the way.
Libraries and maturity — Each language fits well for certain use cases based mostly on the kinds of projects their surrounding communities espouse. Java excels well for a lot of orchestration and network logistics-based requirements — database support through JDBC interface standardization and projects like those that fall under the Apache Foundation help Java serve that purpose. The same goes for Python and data analysis and statistics, as does Haskell with grammars, regular expressions and compilers. A language’s adoption rate and community size is also a great indicator of whether one should espouse a project under such a language — smaller language communities mean less help from the outside world when something breaks.
Garbage collection is the act of the program reclaiming memory space on its own, without the developer explicitly doing so (as you would in “systems languages” like C and Rust). In programming languages like Python, PHP and Swift, determining whether to deallocate objects or not are based on the count of references — the idea of reference counting. However, even while sharing this similarity, they differ on their respective implementations — specifically, with respect to how they handle classical memory leaks (objects with no useful references to them from the outside world but still prevent the garbage collector from cleaning them up!).
Python implements reference counting alongside a “stop-the-world” generational garbage collector — ”stop the world” as the garbage collector pauses program execution, kicks in and proceeds to do garbage collection, then resumes program execution and “generational” as the garbage collector mantains 3 separate “generations” — 3 sets of heaps. Generation 0 heap gets checked the most and contains the most “fresh” objects, followed by Generations 1 and 2.
PHP (since PHP5.3) implements a concurrent garbage collector alongside reference-counting, which runs alongside program execution if need be and is localized — not needing to traverse the entire memory space to search for cyclically referring objects by constructing a reference graph. Subgraphs which cannot be reached from the root can be safely eliminated.
Swift also uses reference counting but with no other garbage collection mechanism alongside it, leaving it up to the developer to ensure that objects which cyclically refer to each other are cleaned up through language primitives. Below, we demonstrate the use of a weak pointer — when an object’s “strong” reference count goes all the way down to 0,
Person will be cleaned up (as it is only weakly referred to by
Apartment). This allows Swift to add compile-time decisions as to when and where to garbage collect code, allowing it to save itself for having an actual runtime garbage collector.
There are lots of other garbage collection mechanisms implemented by other programming languages. They can widely affect the performance of an application depending on the use case so understand the memory model of your choice programming language for your use case!
- Build/Package management
Get accustomed to the mechanisms for keeping/tracking dependencies as well as ways to maintain “build” information (package description, how to run unit tests, setup/environment preparation, etc). Python uses pip in tandem with a requirements.txt file for managing dependencies and setup.py for managing environment setups, Haskell uses Cabal for both, Java has Maven and Gradle, Scala has SBT, PHP has Composer, NodeJS has npm — and the list goes on.
Finding ways to localize your development environment is also a must — you might potentially want to run different versions of programming languages depending on a project. Projects like phpbrew for PHP, pyenv for Python and nvm for NodeJS give you the power to do just that.
When you install a library for use in one project, you might also potentially not want to have it installed in another project (this is moreso a problem for languages like Python and Haskell who tend to install dependencies globally relative to the current interpreter/tool). Get accustomed to tools like Python’s virtualenv/venv, PHP’s virtphp and Haskell’s Cabal Sandboxes.
- Async I/O
Async I/O allows us to get more I/O bound performance out of our applications. Much like threads, async I/O give us the ability to let another part of our program “run” while we are doing an I/O intensive operation — whether that be making an HTTP request, hitting up a database or anything in general involving the outside world (outside the realm of pure computation that a processor does) that can take considerable time. Unlike threads, async I/O allow for constant memory space usage depending on their implementation as creating threads still have some memory cost in terms of keeping a bunch of metadata.
Each thread mantains its own separate set of registers (mainly for keeping track of where we are in terms of execution in the current thread) and stack information (mainly used for function execution — passed parameters, local variables, etc).
- Functional Programming
Functional Programming is the ability to tell your computer at a high level, what you want it to do. Considering the fact that most applications today handle some sort of data extraction, transformation and loading — especially in a multithreaded/multiprocessing way (as to get more performance out of a computer), following functional programming ensures a lot of guarantees. While their are languages that only ascribe to functional programming (Haskell), most languages leave the level of adherence to the programmer. Most languages today have the most basic primitives for functional programming— through map, filter, reduce for lists, allowing for functions to be passed around like any other value (functions as values) and for functions to accept other functions or return functions like any other value (higher-order functions).
- First pass — look through short-chunked resources and build a mini-project that whet your appetite
Building small applications like command-line apps, “scripted work” like an application that downloads a website’s HTML content, parses it and looks for something interesting or even a “playground”-like experimentation piece of code is also a must. Write small little bits of applications well below typically 300-400 lines of code.
The trick in this stage is to get up to speed with programming language mechanisms that you will use 70–80% of the time and to make you comfortable at the very language itself (while giving you the confidence that you can learn a new language) by being able to accomplish something while giving you a taste as to how or where this language may be applicable.
Since our brain is wired for gratification, we want to be able to feed our brain with rewards as quickly as possible, to keep us highly motivated!
- Second take — look through bigger language-specific resources and build a full-sized project that gives you greater language insight
It would help to write full blown applications — writing a full RESTful backend API, a desktop app, or a web app or a mobile app, for example. Try to use external libraries (written by other developers) when need be and get confident with your language’s tooling (how it manages dependencies, how it expects to be deployed to a production environment — whether running on a virtual machine, as a stand-alone binary, or on a given language interpreter, how to write tests, etc). You should strive to work with non-language dependencies — things like databases (like Postgres or MySQL) as well as contacting external HTTP APIs.
- Three times the charm — abstract out concepts and anchor on abstract learning resources
Congratulations - you picked up a programming language and you feel confident in your abilities!
This is the time to reflect and think abstractly, working on what you can refactor out of your codebase into a generalized line of thinking. Think about the performance of your application (and as such, touch things like multi-threading/multiprocessing as well as data structures and time/space complexities). Look at interesting ways the core algorithm of your given app works and see how you can improve it (and delve into things like Graph Theory, Combinatorics, Theory of Computation and Optimization). Resources like Carnegie Mellon’s Parallel Programming course, Stanford’s Introductory Machine Learning and/or Open Source Software University (a listing of courses for a completely free and self-taught education in Computer Science) are highly recommended.
It can also help to look at past or current works — for example, how web browsers and respective HTML/CSS renderings and animations are performed under the hood or looking into the mechanics of the Apollo Guidance Computer. The possibilities are endless — all at the point of getting you to appreciate that all programming languages have their use case, that not one language trumps all and improving your skillset so that your next project (maybe, your next programming language experience) is more refined.
That’s a wrap! We went through the differences between programming languages and as such, how to categorize and choose one above the other given your use case. We then talked about pervading concepts across programming language that we can port over as we learn more and more languages. Last but not least, we learned about how to learn when picking up a new language!
Don’t hesitate to tell fellow readers what you’re working on below. Also, if you’re on GitHub, psst - I’m on GitHub as well (github.com/alastairparagas)! If you liked the article, please don’t forget to give it a clap 👏 (and if you really liked it, please smash that clap button and give it a bunch of claps 👏👏👏👏👏)! If you didn’t like the article or something about it, please don’t hesitate to comment below!