Software engineers often need to learn more than one programming languages during their career. This requirement can arise when changing companies and working on a different stack or when a new language is being adopted gradually.
Switching to a new language is not an easy decision and it may affect the entire software development lifecycle. Time & effort needs to be spent to ramp up the engineering team’s understanding of the language syntax, design/testing patterns & optimal code architecture. Perhaps more importantly, the team needs to familiarize with the operational behavior of the runtime under production workloads!
The more widespread adoption of service-oriented architectures and the advances in operational automation nowadays allow the use of a wider range of languages and runtimes. Some example factors that may affect this design decision can be the following:
A runtime with efficient concurrency support can provide very high throughput with a very low resource footprint, depending on the workload characteristics (e.g. NodeJS, Go).
A strong ecosystem and community may supply mature, well-tested, efficient libraries and frameworks that can boost development considerably. For example, Python has been so far a very popular choice for building Machine Learning services and enabling Data Science research.
Learning a new programming language and familiarizing with the runtime and the ecosystem can be both interesting and challenging at the same time. Obviously acquiring expertise requires time and frequent hands-on work on real use cases and tasks. Let’s see if we can approach learning in a way that helps us become efficient faster!
Getting Started
Finding resources and reading material. Several languages offer abundant web-based resources for running code snippets, a structured language reference and extensive tutorials. I would recommend starting with a book that’s focused on teaching the fundamentals of the language and allows you to gradually build up your knowledge. The Learning
series by O’Reilly is usually structured this way, but if you’re already an experienced software engineer perhaps you’d be more comfortable with more fast-paced, advanced material. I would definitely advise against skipping the fundamentals completely though, as you’ll keep stumbling on minor issues.
Find a technical book that’s focused on teaching the language fundamentals.
Installing the language runtime is our next step, as we need to be able to execute any code snippets built during the learning process. While you can use a browser-based, language playground, you might find it beneficial to familiarize with runtime installation and its behavior.
Prefer a local installation of the language runtime.
Setup your editor/IDE to work with the specific language. A recommendation here is that your configuration allows you to quickly jump to type & function definitions of standard or third-party libraries and read the code. You can see an example with Jetbrains Pycharm here. There are some significant benefits of having this capability:
You can read the documentation and actual code for a standard library function, without leaving your editor window. The standard library tends to be also highly optimized and idiomatic, since it’s maintained by experts on the language.
If you have enabled a debugger, you can do start a step-by-step debugging session to confirm how the code works.
Start a small project. While writing simple functions, loops, if statements is definitely a good first step, you’ll probably need a small toy project, such as a Todo List manager or something similar that requires CRUD operations and working with data.
The Basics
First things to cover:
Primitive types
Variable initialization
Arithmetic and logical operators
If-else statements
Loops
Iterables
While not ideal for production-ready applications, translating already well-understood code from a different language is still useful for learning, since you’ll start mapping already familiar concepts right away!
Mathematical problems are usually the easiest to implement at first, e.g. calculating the sum or the median of an array of integers which will require some use of loops, conditional statements and data structures like arrays. It’s better to start with pure functions at first and avoid any kind of state management. Usually such problems are ideal since they can be modelled using a procedural approach. Procedural programming is universally available across languages and even on heavily object-oriented languages (e.g. Java) you can define functions as static methods attached to classes.
Do not spend effort on idiomatic syntax and performance optimizations at this stage.
You can then proceed with learning about custom type & function definitions, inheritance and object model (if applicable).
More Advanced Topics
Writing tests: familiarizing with test runners and assertion libraries. Some languages have built-in testing frameworks some others don’t.
Errors/exceptions and stack traces: being able to identify the root cause for a failure is essential for your development workflow and production deployments alike.
Mutable and immutable types: mutability is an important concept and differs among languages. It affects how code is structured and when not thoroughly understood can lead to serious bugs or memory issues.
Concurrency model: most languages support concurrency either via green or OS threads, multi-processing, async I/O. It’s useful to familiarize with the fundamentals, just to be aware of what capabilities are supported.
Variable scope: some languages can create locally scoped variables in statements and blocks. Incorrect scope understanding can become a source of errors.
Error-prone anti-patterns: for example you can find here a list of common Python gotchas.
Common Mistakes & How to Avoid Them
Reinvention
The uncertainty of entering an unknown ecosystem can often lead to some tendency to reinvent. If you find yourself implementing something seemingly very fundamental, you may need to take a step back and do some research first. Unless you’re solving a very unique problem, the chances are that there are already available libraries that can help partially or fully address your requirements.
Translation
You can sometimes notice that a codebase is heavily influenced by a language different than the one it’s being written in. It takes some time to get used to another language’s optimal code architecture & patterns. How do you know if you’re writing something in the recommended/most popular way? Well, sometimes it won’t be easy to understand whether something seems too difficult because it shouldn’t be done this way or because you’re just struggling due to inexperience. Sometimes linter tooling can help point you to the right direction, but it’ll require mostly reading (a lot!) existing code from the standard library and very popular OSS projects, as well as articles from language experts.
Full Software Development Lifecycle
Do not underestimate the effort and learning curve to make the code testable, in addition to learning the whereabouts of the testing frameworks. In-depth knowledge of the testing frameworks is necessary in order to give you confidence that the code works as expected. Although your knowledge on testing practices is definitely transferrable, what often differs greatly is testing frameworks, stubbing/mocking libraries and injection patterns, and of course assertions.
Different performance characteristics and runtime design may also require different production stack. For example, NodeJS is very performant for async I/O but is single-threaded when it comes to CPU-bound operations and the same applies to Python with ASGI. Thus, production-ready application servers must be capable of spawning multiple sub-processes in order to take advantage of multi-core machines.