About a week or two ago, I found myself prompting Google and other search engines with questions like, “what is the best programming language?”, “how to choose a programming language?”, “how to interpret performance benchmarks?”, et ad nauseam. I even took a few cheap-o “what programming language are you?” type quizzes (I, in fact, created this “cheap-o” quiz).
Gimmicks aside, being a non-programmer, I neither have the luxury of being dictated languages to learn nor the opportunity to learn perhaps dozens of languages throughout my career. This lead me down a path in which I felt compelled to choose once, and choose right. In my visionquest to find the “right” one, and after weeks of research, I am no closer to nor am I any more certain about any of these answers. In failure, however, I discovered that I had framed the problem incorrectly. Instead of thinking about learning a programming language as a linear endeavor or as an exercise in academia, I should have remembered the wisdom passed down by my illustrious ancestor, the caveman: “every problem looks like a nail if all you’ve got is a hammer”. Even our great fore-bearers knew that it is better to adopt a synergistic array of tools, so why wasn’t this immediately obvious?
Short answer: because I lacked an understanding of programming fundamentals. Because I only had a hammer (Excel/VBA), every problem looked like a nail. Which brings me to my first point: an understanding of how languages and programming paradigms were developed to address various types of problems provides a framework for deciding upon a complementary assortment of tools that are suited for various tasks.
With these thoughts and questions in mind, I humbly submit my analyses for your evaluation.
While there isn’t really a “best” overall language, it is important to recognize that some tool are better than others, depending on the task at hand.
To grossly over-simplify, the most basic programming language paradigm is the imperative, which tells the machine what to do, and how to do it. At the lowest end of the spectra are machine and assembly. One notch higher on the imperative level are procedural languages, such as C, FORTRAN, and Basic (the lowest of the so-called “high-level” languages). The efficacy and efficiency of any solution at this level is highly dependent on a programmer’s experience, inventiveness and ability.
Object-oriented languages (like C++ or Java) were later developed to enhance programmer productivity through the inheritance of object methods. In this way, a programmer can spend less time on telling the machine how to do things, instead relying on reusable code.
Independently of object-oriented languages, declarative paradigms were developed. These paradigms focused on letting programmers tell the machine what to accomplish, and letting the program decide how best to do it. Functional languages, like SQL, rely less on assignment and more so recursion to achieve this end. Logical languages take this even further, foregoing telling the machine what to do, but rather what an answer looks like.
While I’ve omitted many such paradigmatic distinctions, my point is to convey that no single paradigm allows programmers to solve all types of problems in the easiest and most efficient way. It is sufficient to be able to understand the concept of paradigms as it applies to problem sets; reciting them is secondary.
Figure 1: The Basic Paradigms
Table 1: Matrix for 29 Languages with 15 paradigms
3. Intended Uses
Along the same lines, it makes sense to be aware that various languages were created with different use cases in mind.
Table 2: Matrix for 29 Languages with 15 Intended Uses
Benchmarking is one of the most common ways by which to measure and compare language performance. Most commonly, they measure speed, but can also be used to measure expressiveness, efficiency, and more.
The below graph summarizes languages benchmarks on a number of computationally intensive tasks using three criteria. The raw data for the metrics is available at http://benchmarksgame.alioth.debian.org/. For all the following criteria, lower numbers are better. The criteria are as follow:
- Size(B): Corresponds to the average compiled size of code utilized to run task. It is a proxy for measuring language expressiveness; the smaller the number, the more a programmer can accomplish in fewer lines of code.
- Mem(KB): Measures average memory utilization in kilobytes; lower numbers correspond to more efficient handling of memory.
- CPU(s): Measures the average number of second to run a task; obviously, faster is better.
Figure 2: Efficient Frontier: Speed vs Memory vs Expressiveness
Table 3: Efficient Frontier: Benchmarks*
- Benchmarks evaluated using average performance metrics based on the following computational tasks: binary-trees, binary-trees-redux, chameneos-redux, fannkuch-redux, fasta, fasta-redux, k-nucleotide, mandelbrot, meteor-contest, n-body, pidigits, regex-dna, reverse-complement, spectral-norm, and thread-ring. Raw data, accessed February 17, 2013, available online: http://benchmarksgame.alioth.debian.org/u32/summarydata.php
Admittedly, a simplified analysis such as this is prone to the “flaw of averages”, meaning that averages fail to demonstrate dispersion and outliers. A few sources have managed to overcome this by displaying performance metrics using star charts:
There are numerous sources and ways to benchmark a programming language’s performance (see: http://dan.corlan.net/bench.html; and, http://julialang.org/). However, there is no way to expediently untangle all of the inherent biases in them (see explanation here). AttractiveChaos combats some of this bias by testing fewer languages with different compilers as a sensitivity/robustness test.
5. Other criteria
Aside from traditional empirical benchmarking, there other considerations. Some of which are quantifiable, while other lend themselves more to qualitative analysis. Below is a short list of criteria by which to compare various languages. This list is not exhaustive nor, with the exception of productivity, ordered in any particular manner.
- Productivity – For problem-centric programmers, productivity is probably the most important factor. It is also the most inter-related with other criteria.
- Expressiveness – On the most basic level, an expressive language is a concise language, meaning that it can do a lot more for less code. Programmers disagree on the correct way to measure expressiveness, whether through the number of code blocks, number of characters, or overall size of the compiled code (see one possible solution). Wikipedia also has an interesting take on measuring expressiveness.
- Aesthetics – RoBlog has a nice post on this facet of a language: “A beautiful language has a clear and uncluttered syntax — neither more verbose than necessary nor more terse than readability allows. A beautiful language has clear semantics, …should be expressive…, [and] should be efficient.”
- Maturity – A mature language has been vetted and boasts a large and well-documented library. FORTRAN is often cited as having a very mature library for numerical analyses.
- Domain Specificity – A language should respond to your specific needs, possibly including: Scientific/Quant Analysis; Text/Language Processing; and Image Processing.
- Speed/Efficiency – Raw speed may not always be necessary, but should never sacrificed unnecessarily. As compilers become more sophisticated and robust, even some of highest level interpreted languages can come within a factor or two of C/FORTRAN.
- Portability – Portability may or may not a necessity, but it never hurts to be able to deploy across multiple operating systems, and even within a web browser. Portability is one reason why Java has been so well received.
- Scalability – This is a concern when moving from a research/prototype environment to deployment. Transcribing code from one language to another may be a good academic exercise, but surely one inconvenience as well.
- Ease – One way of measuring a language’s speed and efficiency starts not at compile-time, but rather with coding. The ease at which a language can be learned is important if this is how you measure productivity.
- Flexibility – Can your language address a broad array of problems, or is its utility narrowly confined?
- Price – Even when price is no object, it is. With so many open-source projects to choose from, one should be able to justify an extra expense.
- Extensibility – Does your language play well with others? Can you use alternative compilers or call on non-native libraries without much trouble?
- Acceptance/Popularity – While many might eschew popularity as a valid criterion, it is in most cases a useful indicator for traction in any of the previously mentioned categories.
6. Note on Popularity
It has been my experience that the deciding factor for many people has been language acceptance and popularity. Admittedly, popularity is somewhat of positive reinforcement loop: popular and possibly inferior languages stay popular precisely because they are popular in the first place. Unfortunately, this cycle makes it difficult for new and innovative languages to capture an audience. For example, Julia (http://julialang.org/) which was released in 2012 offers many promising features and advancements in the realm of numerical and high-speed computing, but will have a difficult time replacing the deeply embedded Matlab, R, and FORTRAN. Also, OcaML is often cited as a superior functional language, but has yet to fulfill its potential due to immature and sparse libraries. The examples for this kind of phenomenon are endless (via-a-vis Google’s Go and Dart; DARPA’s responses to FORTRAN: Fortress, Chapel, and X10, etcetera…).
On the other hand, language popularity is good proxy for traction in a number of performance and aesthetically related characteristics. Given that popular languages may or may not be inherently “superior” to their less popular counterparts, their are a lot of good reasons for going with the crowd. One of the main reasons is due to that they are taught in schools. Equally as important is language maturity. If a lot of people are using a language, it is likely to feature mature libraries, a vibrant community presence, and well documented support. In my personal experience, the best resource for trouble-shooting code is simply typing the issue into Google. If the language is in wide use, their is high likelihood that someone has already solved a similar, if not almost identical, problem.
Google Trends, LangPop, and Tiobe provide some good ways to benchmark popularity. The chart below, courtesy of RedMonk, displays 2012 language popularity as measured by the amount of activity on GitHub versus StackOverflow.
Figure 3: Language Popularity on RedMonk
Chart courtesy of RedMonk. Online available: http://redmonk.com/sogrady/2012/09/12/language-rankings-9-12/ (Accessed February 15, 2013).
7. Programming as an Exercise in Project Management
- Prototyping: pseudo-languages like Excel, Matlab, Mathematica, or R are well suited to rapid-prototyping for data analysis types of applications.
- Testing: scripting languages like Ruby, Python, and Perl can often be used as “glue” to implement prototypes into test environments.
- Integration/Deployment: once prototypes have been vetted, they can be interpreted and implemented in other languages by “specialist” programmers.
The tacit wisdom of adopting “problem-cetric” approach premises on the notion that the proper role of automation is to enhance productivity, not stifle it. In other words, we are the masters, they [computers] are our servants. Trivial and academic pursuits aside, the questions for choosing programming languages are the same ones your prodigious ancestors pondered when flint first struck stone, or when the tension of the sapling was first strung into snare then bow. Likewise, whether your prerogatives are for aesthetic appeal or raw computational speed, the goal, from inception to coding to run time, is always to carry out the most of our productive interests for the least amount of effort. Ask yourself: “Is optimization of speed premature while productivity languishes?”; “Are semantic elegance and lexical simplicity just means by which to further our productive capacity?” Eschewing the “one-size fits all” paradigm, we should be able to call upon a diverse array of tools; ones that compensate for the shortcomings of others, and that complement each other in ways that enhance our ability to solve problems.
In keeping with our problem-solving analogy to choosing a programming language, I am reminded of a recurrent foil with my childhood friends: “If you had to survive in the wilderness and you could only take one tool with you, which one would you take?” There is no one right answer to the existential problem, but we can intuit some heuristic rules which center on balancing domain-specialization, experience-level, and versatility.
In deciding which tool (language) to take into the wilderness (workplace), it is important to attempt to match the types of problems one may encounter with the skill-sets one already possesses. A bow and arrow (specialized application) is great for a trained bowman (professional programmer), but basically useless otherwise. Moreover, a bow (specialized application) is also useless for anything but hunting and self-defense (the applications for which is was designed).
Often, we do not know exactly what types of challenges we will face. Versatility is therefore our best countermeasure against this uncertainty. For example, a carpentry hatchet (crude yet extensible scripting language) is useful in more types of wilderness (workplace) scenarios than the bow and arrow (specialized application).
The standout hatchet tool, in my opinion, definitely corresponds most closely to Python. The Protocols for the Establishment of Python (PEP) 20, (i.e., “the Zen of Python”), elegantly captures the essence of balancing robustness with simplicity and versatility. In fact, I might go as far as to claim that PEP-20 has been a major factor in Python’s success.
After becoming sufficiently proficient with the hatchet, its limitations will become apparent. Fortunately, there is a wonderful multitude of additional options to choose from (in addition to the multitudes of scenarios which invalidate these heuristic rules). Additionally, the more tools one masters, the easier it becomes to master additional tools. As the learning curve flattens, it is natural to begin realizing how incorporating domain-specific tools actually lessens the overall burden of work while also improving the end-result.
Like I said, a caveman could do it.