As a programmer who tries to make pragmatic and rational decisions, I’m very rarely pragmatic or rational about choosing a programming language.
I don’t think this is uncommon. It’s easy to defend a language you’re familiar with by painting a dystopian picture of the maintenance hell of polyglot systems and the Faustian pact of technical debt when adopting something new. On the other hand, it’s far too easy to over-optimistically claim that a new programming language will solve all of your problems: it’s more performant, easier to test, and it’ll keep you on your toes.
It’s my belief that we can be more critical with how we discuss the benefits of different programming languages and I think it’s important we recognise quite how extensive and difficult these discussions can be.
Below, I suggest five questions as a loose framework for evaluating and presenting a new language option for you or your team. That said, please note that this is not a guide for new developers looking for their first language to learn as this should be influenced by very different considerations.
Should I even be considering a different language?
A programming language is only a part of software development and, as with all decisions in technology, it is important to consider the different ways in which software decisions can impact a business.
You should think about stopping reading if:
- You already have a large codebase to maintain which is written in a specific language and your team has valuable experience with its tooling, libraries and frameworks.
- You have deployment experience with a particular language ecosystem and redeployments might be complex, risky or expensive.
- You might be limited in hiring or on-boarding future developers for an uncommon language, especially if you plan to use niche frameworks or design patterns.
- You are proposing a language which is used by few other teams in the domain, meaning you will need to consider scalability, deployment and security by yourself.
- Developers are more valuable than servers.
- A positive developer experience is one of the most important considerations in good software architecture.
- As much as possible, allow other teams to make your mistakes before you do.
Good, now let’s do the geeky stuff.
How will the language design make my life easier?
Language design is complex and compromises are always inevitable. Different languages have different priorities and intentions, and this should be celebrated.
Here are two of the biggest considerations concerning language design.
1. Type Checking
As you will probably know, different languages notice what types you are using in your code at different times. Many statically typed languages have types which are first checked during compilation while many interpreted languages do not.
For example, Java won’t compile if you declare a boolean variable and then attempt to assign to it the value from a method which has a return signature of another type. Conversely, Python won’t ask for explicit types in your source code and it will complain at runtime if a particular usage of a type is impossible or has been used accidentally.
Having a compiler which is aware of the intended data types and signatures in your program can be a great advantage as it can often spot a host of basic but common errors in your code before you’ve even had to run the program. On the other hand, dynamic type systems make patterns such as duck typing and monkey patching in testing much easier.
In my opinion, static type checking options can significantly speed up development. This need not limit you to classic compiled languages: Node and Python 3 have static type checking options with Typescript or the Python typing package.
Wanting to run parts of your program concurrently or in parallel is very common, but finding a comfortable strategy for this is a real minefield. It’s worth considering the issues below when evaluating a language or one of its libraries for concurrency.
- How safe are the concurrency primitives offered? Dealing explicitly with threads can be painful and requires the use of concurrency features which prevent two threads accessing the same object in memory. This can be a challenge to design and debug. Of course, sometimes powerful concurrency features can be provided in language libraries rather than in the language itself.
- Do you need a multithreaded environment? A single threaded event loop allows for performant non-blocking IO without exposing the perils of threads to your team. However, parallel execution of expensive CPU-bound tasks can be tough or require a form of multiprocessing.
- What abstractions can I use to make concurrency easier? Languages which have adopted syntax for channels, coroutines and tasks offer simpler structures for dealing with concurrent actions with as small a performance overhead as possible. However, the implementation of these tools is rarely standardised between languages and it is worth researching whether they are using thread pools, some kind of green threads, or even event loops behind the scenes.
What actually happens to make my code run?
So you’ve looked into the language design and how it may or may not make your life easier. But however good the language design is, programs have to run somehow. Rather than jump into the mindless foray of benchmarking, here are some more pragmatic things to think about.
1. Virtual Machines and Interpreters
Languages which run in environments such as virtual machines or interpreters can (but not necessarily do) provide portability benefits. This was a key argument made at Java’s inception: your program could run on a number of different devices due to the Java Virtual Machine (“JVM”) and Java Runtime Environment (“JRE”). This meant you didn’t need to necessarily build your program for different architectures or operating systems.
As an aside, it is also worth researching whether there are security benefits or pitfalls for using a particular virtual machine, interpreter or other software environment.
NOTE: As well as improving clarity, I have removed my comments about performance in virtual machines, interpreters and software environments as I think this is too complex a topic. I have also altered the phrasing to no longer make it seem that virtual environments, interpreters or software environments necessarily provide security benefits, as this may not be the case and the topic needs to be considered from a number of complex angles.
2. Managing Compilation Steps
Compilation is an important component in many languages, regardless of the what the source code is actually compiled into. However, it can be a painful process.
If you’ve used a language with a significant compilation step, you’ll appreciate that your everyday experience with a compiler is one of the defining features of your relationship with a language. Compilation speed as projects grow could be easily overlooked, as can managing target profiles for different architectures if that is necessary.
3. Using Diverse Performance Metrics
- What is the memory overhead of my program? The JVM, for example, can use a significant amount of memory which can require optimisation.
- How easy is it to extend the language with performant extensions? If you really want something to run fast, native extensions can be a powerful pattern. Python can be computationally slow with its own data structures but provides tooling for creating libraries with native extensions for optimised calculations. Similarly, Node enables native extensions in C++ and projects like Neon provide incredibly quick bindings to Rust.
- What load can the language sustain? Particularly with server development, it’s no good to be fast if the language environment does not make it easy to sustain numerous requests at once. For example, spinning off a new thread for every request can be hard to sustain at extreme load, leading to some of the other concurrency primitives discussed above.
Does the language support design patterns which will scale?
Language design and implementation has a direct impact on how easy it is to structure and scale large and complex applications.
NOTE: the content below in this section has been changed to no longer suggest that interpreted languages are more flexible with what programming patterns they support and no longer indicate that they allow structuring code in modules rather than classes. These points are sometimes true but not by necessity, and it is not always recommended to avoid Object Orientated or class-based patterns by replacing classes with modules just because you can. I’ve also removed my suggestion that a couple of particular languages are better suited to the Dependency Injection pattern; this pattern is supported by many languages supporting Object Oriented programming.
As codebases scale, it is important to consider what design patterns can be used to organise and structure your project. Different languages support different programming paradigms and there are different ways of building large applications within each paradigm. Class-based and Object Oriented languages (such as Java) can make use of Object Oriented design patterns. Other languages, such as Python, may support multiple programming paradigms and the architect must decide what type of patterns to use as a codebase grows. When considering a language, it is vital to research what patterns other developers have used to scale big applications and how easy they are for your team to adopt.
Another issue to consider for large projects is testability. Different languages recommend or require different patterns to test code, especially when creating mocks and stubs of objects or functionality.
How might tooling and standards increase the time to ship code?
To my mind this is one of the most important points: you may have a beautiful and performant language but if it’s tough to actually write code, it won’t last long.
Here are some essentials:
- What tools can I use when writing code? IDE support, widely-adopted linters, and debuggers should not be undervalued.
- How do I manage dependencies? I think there are some excellent languages out there which fall down at this hurdle. Effective dependency tooling should be standardised, have built-in security features, and have a clear philosophy about version management.
- How opinionated is the community? Do not underestimate the power of official style guides, code review tips, and open source projects which can be used as examples.
- Does the language consider security? Cybersecurity considerations are an essential part of a language and its environment.
As mentioned at the beginning of this article, it’s essential that we talk about programming languages with reference to the broad spectrum of issues involved. There is no one language to rule them all and we can only ever talk about the philosophy behind a language and whether its adoption would give us a meaningful advantage in solving the problem we are facing.