Being a polyglot programmer
Introduction
Today I write program code in multiple languages – usually Java, JavaScript, Python, C, C++, and x86 assembly. This wasn’t always the case, and my first 5 years of programming experience were spent almost exclusively on Java. Although I did get exposed to other languages occasionally, it was only much later that I got into the habit of regularly programming outside of Java. Nowadays I choose the language for new projects based on convenience and requirements, and when I publish algorithms I go the extra mile to provide code in multiple languages for the reader’s convenience.
I believe there are benefits to being a multi-lingual (polyglot) programmer. Because not everyone has the time, circumstances, or motivation to be multi-lingual, I hope this article will raise awareness of the phenomenon and start discussions on the pros and cons of this way of working.
Contents
- Introduction
- Overall observations and opinions
- Getting easy access to features
- Adopting habits from stricter languages
- Avoiding language-specific quirks
- Concepts better explained in another language
- Seeing what a language is missing
- Gaining insight into an algorithm’s core
- Expanding the audience inexpensively
- Code examples
- Fibonacci function
- List manipulation
- Classes and objects
- Libraries published on my other pages
- My opinions on specific languages
- Java
- Python
- JavaScript
- TypeScript
- C
- C++
- Rust
- Haskell
- C#
- Personal history of languages
- Final thoughts
- More info
Overall observations and opinions
- Getting easy access to features
-
Each language has a different set of features built into the syntax and standard library. Using built-in features is much easier than using an unwieldy syntax or importing a third-party library.
- Literal lists and dictionaries: Python, JavaScript, C++
- Unicode text handling: Python, Java, JavaScript
- Regular expressions: Java, Python, JavaScript
- Efficient int and float arithmetic: C, C++, C#, Java
- Big integer arithmetic: Java, Python, Haskell, JavaScript, Mathematica, C#
- Complex numbers: Python, C, C++, C#, MATLAB
- File input/output: Java, Python, C, C++, C#
- Network sockets: Java, Python
- JSON handling: Python, JavaScript
- ZIP, DEFLATE: Java, C#, Python
- Easy to compile/run on Unix systems: C, C++, Python
- Adopting habits from stricter languages
-
-
Dynamically typed languages like Python and JavaScript don’t force you to specify the type of every variable. In statically typed languages, such information is mandatory, and I find it helpful to carry over the habit. So I would mentally label variables as being, for example, integer, string, or list of Booleans. By keeping track of variable types clearly, type errors are essentially eliminated.
Sometimes I explicitly write out the variable types in code comments (e.g. at the top of qrcodegen.py). But this informal information can only be processed by humans, not by computers. Although Python and JavaScript were initially released without machine-readable type syntaxes and corresponding checkers, now they exist and work quite well. In particular, Python 3 added an optional type annotation syntax and Mypy is one available checker tool; Flow is a checker for JavaScript where types are embedded into comments; TypeScript is a new language that extends JavaScript by adding typing and extra features, and such code gets type-checked and then compiled down to regular JavaScript
-
In the minority of languages like Java, C#, and Haskell, the condition of an if/while/for statement must evaluate to a Boolean-typed value – not an integer or an object. For example
if (true)
is legal, butif (0)
is a compile-time type error in Java. Most other languages allow various types of values to be interpreted as a Boolean condition in if/while statements, e.g.void *ptr = (...); if (ptr) { ... }
in C, ornum = 0; if num: ...
in Python.Different languages have different notions of what values are treated as true or false. For example
0
is treated as false in Python and JavaScript, but surprisingly true in Ruby. For example[]
(empty array/list) is treated as false in Python but true in JavaScript. For example0
isfalse
in JavaScript butnew Number(0)
is considered astrue
. However, at leastnull
in JavaScript,NULL
in C,None
in Python, andnil
in Ruby are all consistently treated as false.The easiest way to guarantee consistent behavior across all languages is to ensure that the condition of an if/while statement is always a Boolean value. This means writing explicit comparison tests as needed, avoiding the shortcut way provided by each language’s semantics. For example in Python, instead of writing
if obj:
, we should explicitly write outif obj is not None:
. For example in C, the codex = 3; if (x) { ... }
should be expanded out tox = 3; if (x != 0) { ... }
. This kind of explicit value testing is already mandatory in Java, and following the same practice in other languages makes code more robust and much less likely to be misunderstood or mis-translated. JavaScript only has floating-point numbers, not integers (though it has bitwise integer operations). Most other languages distinguish between floats and ints, and observing this distinction in JavaScript is helpful for writing correct, efficient code that can be potentially translated to other languages.
Python uses code indentation to denote block structure, so correct indentation is mandatory by design. In other languages it is a very good idea to maintain perfect indentation all the time too, otherwise the code becomes confusing and messy to read.
Null is a pervasive problem in Java because it is an implicitly legal value for any reference type, even though it is rarely useful. Most APIs are designed to require non-null arguments and always return a non-null result, hence I find myself continually writing boilerplate comments and assertions to outlaw null values where they’re unwanted. Value types in Java (like
int
) can’t be null, but many interesting data structures (e.g. arrays) can’t be expressed as value types. Languages like Rust and Haskell have better type systems; they don’t allow null values by default, but you can consciously opt in using theOption
andMaybe
wrappers, respectively.Encapsulation isn’t taken very seriously in Python or JavaScript. There are idiomatic ways to differentiate public and private members, but they are not always used by coders. Compare this to C++, C#, Java, and Rust, where declaring members as public or private is taken seriously at the outset.
Python and JavaScript have duck typing, so inheriting from a blank abstract superclass is usually optional. But this design pattern is mandatory in statically typed languages like C++, C#, and Java; moreover declaring the inheritance is useful in expressing the data type relationships and design intent to a programmer reading the code. A Python example can be found in my optimizing brainfuck compiler in the Command class.
-
- Avoiding language-specific quirks
-
JavaScript is one of the few popular languages where reading an uninitialized variable or non-existent array index yields a legal
undefined
value instead of throwing an exception. Relying on this behavior is ugly, and it poorly translates to other languages. For example, instead of writinga[3] == undefined
, you should writea.length <= 3
.JavaScript lets you declare a variable multiple times within a function (e.g.
var x; var x;
), which has the same effect as declaring it just once. In Java this kind of code is a compile-time error. In C/C++ this is an error if the variable is re-declared in the same block, but if declared in a nested block then a new variable shadows the old variable (which can be dangerous).In JavaScript and MATLAB, setting the value at a non-existent array index (for example
a[a.length] = 8;
) will automatically grow the array. This is even faster in some JavaScript engine implementations. However I think this code is poor style and translates poorly to other languages; it is much clearer to writea.push(8);
.The
str
type in Python 2 performs double duty of representing byte sequences and ASCII text strings. This leads to sloppy data typing and subtle latent bugs. In contrast, Python 3, Java, and C# clearly delineate byte sequences from Unicode text strings by design.Java’s
String.split()
method will remove trailing blank elements by default, unless a negative number is specified as the limit. This violates the intuitive notion of string splitting. By contrast, the behavior of string splitting in Python and JavaScript corresponds to what you would intuitively expect.
- Concepts better explained in another language
-
In Java, the distinction between values and references can be confusing for the beginner programmer. Even as he matures in Java, the two behaviors are likely to be seen as magic. On the other hand if he worked in C or C++, then he is forced at an early stage to understand the distinction between an immediate value/
struct versus a pointer to a value/ struct. The Java documentation for
equals()
andhashCode()
provide more helpful details on how to write conforming method implementations than Python’s documentation in the equivalent methods__eq__()
and__hash__()
. Also, there are more readily available tutorials on implementingequals()
in Java.Java is a mature, thought-out, and well-documented object-oriented language with a managed runtime. Python is object-oriented and has a runtime too, but its documentation lacks the depth and rigor that the Java ecosystem has. I find that concurrent programming (threads, locks, condition variables, etc.) and garbage collection (when are objects reclaimed, finalizer methods, weak references, etc.) are inadequately documented in Python, rarely used, and/or rarely discussed. I learned about these concepts largely from the Java ecosystem (from official documentation and third-party articles), and found it helpful to mentally translate the concepts in the Java-oriented articles into the vocabulary of the Python language and libraries.
In Python you might be able to argue that some simple-looking operations don’t need locking in a multi-threaded environment. For example, putting a value into a dictionary (like
d[x] = y
) looks deceptively like an atomic operation supported by the language. It is likely to be correct in the CPython interpreter because the dictionary-put method is implemented as a C function, and C functions cannot be preempted even if they need to execute numerous instructions. But the Java equivalent of this code (which would be the method calld.put(x, y);
) is strictly thread-unsafe, and must be surrounded with an appropriate lock and unlock.
- Seeing what a language is missing
-
Java’s BigInteger library is my favorite implementation out of all bigint implementations that are built into a programming language (third-party libraries don’t count). It was released very early and contains numerous useful features like bit length, popcount / Hamming weight, primality testing, modular inverse, and so forth. By contrast, C#’s BigInteger library was released very late and only contains basic arithmetic, without extra features. Python has had bigint support from early on, but still lacks the extra features.
Java and C++ have sorted sets and maps, which are useful when you need a mutable collection and be able to iterate over all the keys in sorted order. Python only has hash-based sets and dictionaries, with no plans to add sorted ones into the standard library.
When traversing a container (such as
ArrayList
), Java’s iterators support element removal and other operations. Python’s iterators are defined in a confusing way, and don’t support element removal or seeking. On the other hand, Python supports generators while Java doesn’t. Generators/coroutines make it much easier to express certain streaming algorithms, such as incremental data compression/ decompression. Python and Haskell have a built-in type for fractions / rational numbers, while Java doesn’t. Having to implement or copy a fraction implementation every time it’s needed in Java can get tedious.
Python and JavaScript have first-class function objects and library functions to manipulate lists using functions (e.g. map, filter, reduce). Java has clunky anonymous classes and had few higher-order functions in the standard library, until lambdas and streams were introduced in version 8.
- Gaining insight into an algorithm’s core
-
By expressing an algorithm or data structure in multiple languages, you will discover which parts stay the same and which parts change. Parts that usually stay the same include the control flow, data flow, and data structures (array, dictionary, tree, etc.). Parts that change are the code syntax, library calls, stylistic conventions, and miscellaneous idiosyncratic features of a language.
The explicit static typing in C, C++, C#, Java, etc. is visually noisy and can hurt readability when glancing at the big picture of an algorithm. But they are useful when drilling down to the details to produce correct runnable code.
Going a step further, some languages denote mutability in the type system; C and C++ mark read-only structures as
const
, and Rust marks mutable structures asmut
. These make it clear as to whether a particular function will change an argument object or not. By contrast, object mutability is only conveyed informally in Java, JavaScript, and Python, rather than being enforced mechanically. However, the price to pay forconst
qualifiers is more syntax in the code and more careful thinking when designing logic.The explicit memory deallocation in C can be a distraction that makes algorithms harder to understand. This is much less of a problem in C++ due to RAII and containers like std::vector, std::string, smart pointers, etc. This problem is non-existent in garbage-collected languages like Java, JavaScript, Python, etc., because memory deallocation is almost always implicit (except for developers who implement buffer structures like
ArrayList
).All the popular programming languages use IEEE 754 binary floating-point arithmetic by default. Hence numerical algorithms will behave almost identically in any language, except for tiny differences in the approximation error of math library functions (e.g. exp() being different by a few ULPs). In particular, it means that if 0.1 + 0.2 ≠ 0.3 in Python when using 64-bit floating-point numbers, then the same problem will occur in JavaScript, C#, et cetera.
Java’s lack of operator overloading makes BigInteger arithmetic look ugly and unreadable. By comparison, bigint arithmetic is very readable in Python and Mathematica due to native support.
The act of porting code to another language gives me an excuse to re-read and think about the entire codebase. This often leads to simplifications, small bug fixes, better comments, and other improvements to the code.
- Expanding the audience inexpensively
-
Compared to most programmers, I spend a lot of time designing algorithms in addition to banging out code. For example I worked on data structures like AVL tree list, broad techniques like Huffman coding, and non-trivial real-world standards like QR Code. The time that I spend on the math and algorithms is a large one-time cost that is independent of whatever language I choose to implement the ideas in. After I draft my first implementation, I resolve the bugs in the architecture, data flow, computation of correct values, low-level logic, etc. This process of coding is costly because it takes deliberate effort to turn a pile of informal mathematical/
algorithmic ideas into a concrete implementation in a real, executable programming language. But after creating one working and debugged implementation, it’s smooth sailing from there on out. Adding another language implementation of the same concept is quite cheap compared to all the upfront preparation and initial debugging work that went into the first implementation. My usual approach to porting languages is to copy and paste the code, and work line by line to fix the syntax and change the library calls. For example when translating from Java to Python, I would replace
if (x)
withif x:
,&&
withand
,lst.add(e);
withlst.append(e)
, delete all the types, and so on. My codebases usually have a runnable main program or test suite (instead of being a passive library), so I can quickly check that the translated version of the code runs properly.By porting my programs to multiple languages, there is a higher chance that a reader understands one of the languages that I published in. This makes it easier to disseminate my math/
algorithm ideas and code style choices to a wider audience. Some kind users on the Internet have ported some of my published code to other programming languages. (For example, my next lexicographical permutation algorithm to Ruby, my simple DEFLATE decompressor from Java to C#, my smallest enclosing circle algorithm to Scala, and my QR Code generator to Dart.) Because other people are willing to translate my code to more languages, it confirms that my goal to provide multiple language versions is valuable in the first place.
Code examples
The first few interactive examples illustrate how similar Python, JavaScript, Java, and C++ are in their syntax and semantics. When I write code in one of these languages, I can port it to another language by reading and writing one line at a time, fixing up the syntax and changing the library API calls along the way. In particular, I don’t need to change the overall architecture, control flow, or data flow.
Fibonacci function
This example demonstrates function declarations, types, variables, integers, arithmetic, if-statements, for-loops, and throwing exceptions.
Python
# Correct for all non-negative n, thanks to native bigint def fibonacci(n): if n < 0: raise ValueError("Negative index") a, b = 0, 1 for i in range(n): a, b = b, a + b return a
JavaScript
// For simplicity in this illustration, // we don't worry about large values of n that // cause rounding errors or numeric overflow function fibonacci(n) { if (n < 0) throw "Negative index"; let a = 0, b = 1; for (let i = 0; i < n; i++) { let c = a + b; a = b; b = c; } return a; }
Java
// For simplicity in this illustration, we don't worry // about large values of n that cause numeric overflow int fibonacci(int n) { if (n < 0) throw new IllegalArgument("Negative index"); int a = 0, b = 1; for (int i = 0; i < n; i++) { int c = a + b; a = b; b = c; } return a; }
C++
// For simplicity in this illustration, we don't worry // about large values of n that cause numeric overflow int fibonacci(int n) { if (n < 0) throw "Negative index"; int a = 0, b = 1; for (int i = 0; i < n; i++) { int c = a + b; a = b; b = c; } return a; }
List manipulation
This example illustrates data declarations, the list ADT, and working around missing features with loops and temporary variables.
Python
a = [2, 7, 4] a.insert(0, 9) a.append(3) del a[1] b = [1, 5, 6] a.extend(b) print(a) # [9, 7, 4, 3, 1, 5, 6] print(sum(a)) # 35
JavaScript
let a = [2, 7, 4]; a.splice(0, 0, 9); a.push(3); a.splice(1, 1); let b = [1, 5, 6]; a.push.apply(a, b); console.log(a); // [9, 7, 4, 3, 1, 5, 6] let sum = 0; for (const x of a) sum += x; console.log(sum); // 35
Java
import java.util.*; List<Integer> a = new ArrayList<>(); Collections.addAll(a, 2, 7, 4); a.add(0, 9); a.add(3); a.remove(1); List<Integer> b = new ArrayList<>(); Collections.addAll(b, 1, 5, 6); a.addAll(b); System.out.println(a); // [9, 7, 4, 3, 1, 5, 6] int sum = 0; for (int x : a) sum += x; System.out.println(sum); // 35
C++
#include <iostream> #include <vector> std::vector<int> a{2, 7, 4}; a.insert(a.begin(), 9); a.push_back(3); a.erase(a.begin() + 1); std::vector<int> b{1, 5, 6}; a.insert(a.end(), b.cbegin(), b.cend()); std::cout << "["; bool head = true; for (int x : a) { if (head) head = false; else std::cout << ", "; std::cout << x; } std::cout << "]" << std::endl; // [9, 7, 4, 3, 1, 5, 6] int sum = 0; for (int x : a) sum += x; std::cout << sum << std::endl; // 35
Classes and objects
This example shows the declaration of a class, constructors, methods, public and private fields, and working with objects.
Python
class Person(object): # Constructor def __init__(self, nm): # Creation of fields self.name = nm self._salary = 0 # Method def get_lowercase_name(self): return self.name.lower() # Method def get_salary_multiple(self, x): return str(self._salary * x) # Method def raise_salary(self): self._salary += 1 def main(): p = Person("Alex") print(p.name) p.raise_salary() print(p.get_salary_multiple(5)) main()
JavaScript
class Person { // Constructor constructor(nm) { // Creation of fields this.name = nm; this.salary = 0; } // Method getLowercaseName() { return this.name.toLowerCase(); } // Method getSalaryMultiple(x) { return (this.salary * x).toString(); } // Method raiseSalary() { this.salary++; } } function main() { let p = new Person("Alex"); console.log(p.name); p.raiseSalary(); console.log(p.getSalaryMultiple(5)); } main();
Java
public class Person { // Fields public String name; private int salary; // Constructor public Person(String nm) { name = nm; salary = 0; } // Method public String getLowercaseName() { return name.toLowerCase(); } // Method public String getSalaryMultiple(int x) { return Integer.toString(salary * x); } // Method public void raiseSalary() { salary++; } } public class Main { public static void main(String[] args) { Person p = new Person("Alex"); System.out.println(p.name); p.raiseSalary(); System.out.println(p.getSalaryMultiple(5)); } }
C++
#include <algorithm> #include <cctype> #include <cstdlib> #include <iostream> #include <string> using namespace std; class Person { // Fields public: string name; private: int salary; // Constructor public: Person(string nm) : name(nm), salary(0) {} // Method public: string getLowercaseName() { string result(name); transform(result.begin(), result.end(), result.begin(), [](char c){return tolower(c);}); return result; } // Method public: string getSalaryMultiple(int x) { return to_string(salary * x); } // Method public: void raiseSalary() { salary++; } }; int main() { Person p("Alex"); cout << p.name << endl; p.raiseSalary(); cout << p.getSalaryMultiple(5) << endl; return EXIT_SUCCESS; }
Libraries published on my other pages
Among my published articles, there are many examples where I implement an algorithm, data structure, or application in multiple languages. These examples show how I map a real-world problem into code in different languages, and how the resulting pieces of code compare in terms of clarity, conciseness, and explicitness.
- Next lexicographic permutation algorithm
-
~25 lines of code per implementation
Featuring arrays, loops, integers, generic types
- Free small FFT in multiple languages
-
~200 lines of code per implementation
Featuring floating-point types and arithmetic, trigonometry, bitwise operations, memory allocation, arrays
Python has a huge advantage in conciseness due to native support for complex numbers and list comprehensions
- Forcing a file’s CRC to any value
-
~200 lines of code per implementation
Featuring file I/O, string parsing, exception handling, bitwise integer math
The C code looks obtuse in string handling and explicit error handling
- AVL tree list
-
~400 lines of code per implementation
Featuring tree graphs, recursion, reference/
pointer manipulation, ADT design, encapsulation The data structure is difficult and the algorithm deals with subtle details. To produce the first working implementation, I spent a lot of time on understanding how the theoretical algorithm works and on debugging my code. When I ported the code to other languages, these one-time costs did not apply.
- QR Code generator library
-
~1000 lines of code per implementation
Featuring object-oriented design, module/
component arrangement, bitwise integer math, arrays There was a large upfront cost in reading and understanding the QR Code specification, along with organizing the logic of the first implementation in an architecturally coherent way
- Overview of Project Nayuki software licenses
-
A list of all code I published on my website, which includes the topic/
title and the list of programming languages About 40% of my pages have multi-lingual program code; the remaining 60% are mono-lingual
In decreasing order of popularity, I use Java (most often), JavaScript, Python, C, and so on.
My opinions on specific languages
Disclaimer: These are personal opinions based on my needs, skills, and weaknesses. There is no single right answer or universal truth about a language or feature being good or bad. The languages are listed in descending order of how familiar I am with them and how often I use them.
- Java
A general-purpose object-oriented language that is safe, concise compared to C, fast enough, and having a decent standard library. I like the static typing (easy refactoring and saves me from so many trivial mistakes), well-defined language and library semantics (predictable behavior and good documentation), platform independence (no need to deal with endianness, bit width, and platform tooling issues), decent speed for low-level numeric processing, Unicode text support, file and network I/O APIs, and competent multi-threading facilities (built-in locks and monitors, memory model, concurrency libraries). Java is my default/
preferred language for many projects for these reasons; also I enjoy using the powerful Eclipse IDE. When I write a program intended to be ported to multiple languages, I almost always start with the Java version first; after it is fully functional and debugged then I translate it to other languages (such as adding header files and prototypes for C++, or stripping out types for JavaScript and Python). Things I dislike about Java include the fact that it is too heavyweight for quick throwaway scripts, it has a bunch of little quirks like signed bytes and type parameter erasure, it doesn’t support ad hoc list and dictionary data structures like Python and JavaScript do, and people commonly write bloated “enterprisy” code in Java. On a final note, I’m not convinced that JVM languages like Scala, Groovy, and Clojure are worth the trouble compared to plain old Java. - Python
A lightweight object-oriented language that is concise, powerful, relatively safe, slow, and possessing a rich standard library. I like that it lives up to its reputation of being executable (and readable) pseudocode, its enforcement of proper code indentation, its practical and feature-packed standard library, the fact that you can declare lists and dictionaries in the code, the power of list comprehensions and generators, and its general lightweight feeling when reading/
writing code. Python is my preferred language for writing short scripts and making small pieces of code to prove a point in a concretely implemented way. I don’t find it appropriate for large projects with many modules and data types. I dislike the Python 2 vs. 3 transition mess (note that I publish 2/3 polyglot code to appeal to both crowds but write 3-only code for private use), how some libraries changed names and features in a subtle way (unlike the stability of Java libraries), how some changes were made gradually and functions added gradually (instead of all at once like in Java), the poor protection against simple typos and data type errors due to the dynamic typing, the lack of standardized documentation syntax (unlike Javadoc), and the slow numerical performance (about 10× to 100× slower than Java, which is in turn slower than C; this is painfully apparent in my Project Euler solutions benchmark timings). - JavaScript
The lingua franca of web programming, JavaScript is a weakly typed, object-oriented, moderate speed language with only a basic standard library. It is superficially similar to Java in syntax and a couple of libraries (such as Math, String, Date), but is quite different in how values, objects, and functions work. The two main things I like about JavaScript are that it enables me to use HTML web pages as a powerful input and output medium, and that its syntax and semantics are reasonably sane (unlike say C++ or PHP). I also like how functions are first-class values, how easy it is to declare a function in an expression, and how smart people created JIT compilers that made JavaScript much faster than similar dynamic languages like Python and Ruby (JavaScript is about 5× slower than Java in arithmetic; Python is about 30× slower). I avoid problems related to JavaScript’s weak typing by thinking about and writing code in the mindset of static typing – in other words, I consider each variable to have one type (such as integer) and avoid doing potentially nonsensical operations that involve mixed types (such as integer < string). I dislike how I/O and event-handling code involves writing numerous function callbacks, sometimes even deeply nested callbacks. Writing vanilla JavaScript is good enough for me; I don’t care to use the popular libraries like jQuery, Angular, React, etc., or safer / more expressive languages like TypeScript that can be compiled down to JavaScript. One time I took a piece of Java code (Panel de Pon puzzle solver) that was type-correct and fully debugged (e.g. no out-of-bounds array accesses) and translated it to JavaScript; due to the non-trivial algorithms involved, it would have been risky to write and debug it in JavaScript as a first attempt.
- TypeScript
-
Essentially an extension of JavaScript that massively improves developer productivity. Primarily TS adds type annotations, generics, and inference, but it also adds conveniences like enums and constructor fields. All JS code is legal (not necessarily great) TS code. None of TS’s new features try to duplicate what can already be done in JS (unlike how C++ significantly overlaps with C). These two conventions significantly reduce the cognitive effort compared to learning a whole new language. As a bonus, the TS compiler keeps up with the latest JS specification (thus adopting newest features in the base language) and it can translate TS/JS features (e.g. classes in ES6, async/await in ES8) into older features (e.g. outputting ES5 code, which is the highest spec supported by Internet Explorer 11).
Personally speaking, I can thank TS for many things. It ended my half-baked workarounds to JS’s lack of static typing, such as type-checking in Java and then porting to JS, and writing informal type comments for JS. TS keeps me sane in developing and maintaining non-trivial (300+ lines) JavaScript applications – because as a codebase gets bigger, I spend more time reading and refactoring existing work, I define more custom types (classes, tuples, etc.), and I find it harder to keep track of all the interface designs (names and types) in human memory – thus I make more mistakes when writing/
editing code, which is largely solved by relying on the TS compiler’s static type checking, rather than by working more carefully and slowly. - C
A straightforward procedural language that is unsafe, well-supported on Unix, good for computationally intensive arithmetic and algorithms, and good for system programming. I like the raw speed and heavy compiler optimizations (especially for numerical algorithms), the Unix friendliness (a C compiler is usually available out of the box), and the clean interfacing with assembly code. I don’t like the poor support on Windows (need to install some distribution of GCC or the massive Microsoft Visual C++ toolset), the undefined behavior (makes arithmetic and so many operations downright treacherous), the bare-bones standard library, the variably sized integer types, the inconvenience of maintaining header files, and the manual memory management (lacking automatic garbage collection).
- C++
A baroque object-oriented programming language that is unsafe, good for applications that need a better way to organize data structures and methods, and a good upgrade path for C programs that get too big. I like it for applications where I could use C but where I need more structure and modularity – such as when I need to define numerous custom data structures and methods on them. I also like RAII for managing object lifetimes, convenient memory-managed containers like
std::vector
andstd::string
, and passing values by reference. All the things that I like and dislike about C also apply to C++; additionally I dislike how slow C++ compilation times are, how C++ fixes none of the problems of C, and how C++ adds many new facilities that are near-duplicates of existing C features.- Rust
-
In a nutshell, it’s C++ done right. It adopts all the good parts of C++ – e.g. familiar syntax, object methods, RAII, immutable referents, value types, generics, ability to call C functions (FFI), no runtime system (e.g. virtual machine, garbage collector). It rejects all the bad parts – e.g. undefined behavior, implementation-defined integer types, header files and prototypes, class inheritance, duck-typed templates. It takes good features from various other languages – e.g. strong memory safety, mandatory bounds checking, array slices, tuples, algebraic data types, pattern matching, build system, package manager. And it adds a few genuinely innovative and helpful features – namely ownership, lifetimes, borrowing, and its model of thread safety.
My personal experience regarding Rust is that many concepts were easily transferred from my existing skills in C++ (e.g. RAII), Java (e.g. fixed-width integers), Python (e.g. slices), and Haskell (e.g. pattern matching). The notion of object ownership/
lifetimes/ borrowing is weak in garbage-collected languages (Java, Python, etc.), grow in importance in C++ (especially due to pass-by-reference, RAII scopes, and move semantics), and are critical in Rust. Just like how static type systems both annotate the code and let the machine check for correctness, Rust’s ownership/ borrowing system takes a formerly informal concept that burdened human programmers and turns into a systematic machine-checkable feature. Because of this, it would make sense to write things like tricky object graph manipulation logic in Rust, pass compilation and behavioral tests, then translate it to C++, Java, Python, etc. - Haskell
A pure functional programming language with a strong emphasis on expressing algorithms in a compact and mathematically pure way. But the high level of abstraction in Haskell makes me constantly feel like I need a PhD in math to read other people’s Haskell code, utilize libraries, or to write any working code at all. I like Haskell’s static typing, algebraic data types, and rich standard library. I dislike how terse and opaque the code is when multiple higher-order functions are used, how I can’t reason about memory usage in long computational chains due to the lazy evaluation, how writing I/O and imperative code needs the programmer to jump through extra hoops, how unclear the mapping between language constructs and low-level machine operations are (thus frustrating the analysis of execution time and memory usage), how nothing seems to be straightforward and there is always a new theoretical concept to learn (e.g. continuation-passing style, type classes, infinite data structures, currying, string-appending functions, etc.).
- C#
A rich, safe, object-oriented programming language that is good for application programming on Windows. I have spent little time programming in C# and have barely seen the possibilities offered by the language (such as LINQ and GUI design). Right off the bat, C#’s naming conventions bother me because namespaces and members are capitalized in the same way as classes are (e.g.
Namespace
in C# vs..Sub .ClassName .SomeMethod() package
in Java). Due to C#’s blatant similarity to Java (I would say Java SE 1.4 is similar to C# 1.0), I compare its features and behaviors heavily to Java. And because I choose Java as my primary programming language, I try to stay within my comfort zone and write in the subset of C# that overlaps with Java. Overall I have a slight negative opinion of C# because it seems to start with Java’s design as a safe and simple C++-like OOP language, and then add tons of features to it that are hard to keep track of (e.g. properties, operator overloading, unsigned types, checked arithmetic, extension methods, partial classes, LINQ). Over the years, it has become clear that Java has taken a much more conservative direction than C# toward language extension – it tries to add new features in such a way that they can be easily mapped (de-sugared) into older features. Also I dislike the MSDN documentation for the standard library (i.e. the .NET Framework) because I find that the wording, presentation formatting, and code samples are much less helpful than the Java SE documentation. If I grew up in different circumstances and C# was my main programming language, I probably would know all its features and use them to make my code powerful and concise. But because I care about other programming languages too, I find many of C#’s features to be distractions that increase my cognitive load without bringing sufficient benefits. But all of this only reflects my opinion of C# as a beginner and can’t be taken as a properly informed opinion..sub .ClassName .someMethod()
As an actual example of where I weigh programming languages for a specific application, see my Wikipedia PageRank article. There I discuss how the project would be expected to fare in Java, Python, and C++. My choice reflected the best balance between speed, safety, and library convenience for the application at hand.
Personal history of languages
I first learned computer programming nearly two decades ago. Very slowly over the years, I learned and got comfortable with more and more programming languages. The years and grades given in the list below are accurate to about ±1 year, because only in retrospect did I realize how my habits were changing:
, Grade 6 (elementary school): Dabbled in web JavaScript programming on the browser side and on the server side (Classic ASP on Microsoft IIS).
, end of Grade 8 (middle school): Learned Java by myself from a textbook. This was the first time I understood crucial programming concepts like array indexing and function calls. I also got better at JavaScript too, but didn’t use it much.
, Grade 10 (high school): Learned Object-Oriented Turing by simply reading the official help manual (bundled with the IDE) for a few hours. I was exposed to this language because my high school had a Grade 10 course that taught Turing, but I skipped the course because I knew the basics of programming beforehand and felt the course was too simple. Nevertheless I was still able to become competent at programming in Turing in my spare time. I translated some of my algorithms like heap sort from Java to Turing, and appreciated the syntactic and semantic differences between the languages.
, Grade 11 (high school): Learned C and C++ while doing programming competitions. I treated the languages as basically like Java but with a few modifications. I knew that numbers, arrays, and control flow were basically the same in syntax and semantics, but I/O functions and pointers were different. My only regret about my experience with C/C++ is that I didn’t understand the deadly concept of undefined behavior until ~7 years later.
, university 1st year: Learned Python in an introductory programming course. Up until now, almost all my programming was done in languages with C-like syntax. Also did a summer job in Python server-side web programming, thus forcing me to really know the language to a level of working competence.
to , university middle years: Took several courses where MATLAB was the main programming language for homework assignments. The course material focused on numerical analysis and matrix arithmetic. MATLAB’s archaic syntax (designed in the 1980s), suppression of mistakes or poor error handling, and the proprietary software licensing all detracted from my enjoyment of the language.
to , after graduation: Did lots of C++ coding and debugging in different jobs, hence refining my fluency and understanding of the language. For example I learned the C++ equivalents of features that existed in C, such as references vs. pointers, iostream vs. stdio, class vs. struct.
: Around this time I discovered that I actually enjoyed programming in multiple languages. When writing articles on the Project Nayuki website and coding Project Euler solutions, I started to put a bigger emphasis on publishing as many programming language versions of an algorithm as reasonably possible.
: Decided to learn Rust as a hobby, being aware that it’s a systems programming language comparable to C++. I was delighted with its clean and sensible design, easy knowledge transfer from other languages, and lack of pitfalls. Although I didn’t build full applications in Rust, I focused on porting my libraries and algorithms to gain experience. Most concepts were easy to translate, but my binomial heap’s manipulation of tree/list node was tricky and required restructuring.
: Learned TypeScript, ES6 classes, and ES8 async/await simultaneously; I felt the need to improve the unwieldy codebase at my job. As I converted the JS code to TS, I reorganized the control flow to be easier to write and audit, added type annotations, and overall made great strides in readability and robustness. I quickly got hooked on the safety and convenience of TS, and began selectively porting my published JS apps and libraries to TS, as well as choosing to start some projects exclusively in TS.
Final thoughts
A programmer who is stuck using one programming language will architect and implement code in a way that optimizes for the particular set of features and bugs in that language. For example a Java programmer can’t apply list comprehensions and will fail to appreciate their conciseness; a Python programmer might create/
modify an object’s methods at run time even when there are safer alternatives that would be applied in a statically typed language; a C programmer might spend effort re-ordering functions so that no prototypes are required even though it may not be the most natural reading order for a human. The mono-lingual programmer will find it hard to distinguish between details of a particular programming language from widely applicable concepts in general computer science. When expressing a piece of logic in multiple programming languages, sometimes the structure differs immensely from language to language (e.g. functional vs. imperative style of manipulating lists), but sometimes the differences are just trivial mappings of a few words (e.g.
bool
in C++ vs.boolean
in Java;else if (...)
in C/C++/Java vs.elif ...:
in Python). Viewed in this light, I have a higher resistance to try programming languages – I want to see something substantially better (and hardly any things worse) before I’m willing to adopt a new language.Some personal weaknesses I’ll admit include: There are too many minutiae (header files, integer type widths, object ownership and deallocation) and pitfalls (all sorts of ways to trigger undefined behavior) in C and C++ that I feel mentally not strong enough to handle it except for special limited-scope projects. I never found a good opportunity to learn some popular lightweight languages like Ruby, Lua, and Go. I’m too averse to frameworks (especially JavaScript web frameworks like jQuery) and frequently use only the base/
vanilla language itself. I tried doing hardcore functional programming in Haskell but feel utterly incompetent in it.