Julia motivation: why weren't Numpy, Scipy, Numba, good enough?

I guess I can give some historical motivation here. When we started working on Julia in 2009, the Python ecosystem was nowhere near where it is today:

  • PyPy was pretty new: 1.0 was released in 2007 – it was the first version to be able to run any substantial Python apps and there were no plans at the time for supporting any of NumPy;
  • Cython was even newer: initial release was 2007;
  • Numba didn’t exist: initial release was in 2012.

Even NumPy itself was a bit rough back then. It’s entirely possible that if the SciPy ecosystem had been as well developed in 2009 as it is today, we never would have started Julia. But then again, there are a lot of aspects of Python that can still be improved upon for scientific computing – and for general computing as well. Note, however, that Julia wasn’t a reaction to Python – @viralbshah has used Python and NumPy extensively, but I had not, and as far as I know, neither had @jeff.bezanson. Julia was much more influenced by Matlab, C, Ruby and Scheme, although we certainly have looked to Python for inspiration on some designs (I straight up copied the path and file APIs, for example). Python is often a good reference for basic sanity in language and API design choices. Here are some of the areas where we felt Julia could do better than existing languages, Python included.

Real metaprogramming. There’s no good reason for a dynamic language not to do this, but aside from Lisps, they don’t. I’m not sure why. The greatness of JuMP is due to Julia’s metaprogramming facilities – and, of course, the brilliant API design work of the JuMP folks to make effective use of metaprogramming to allow targeting different optimization backends efficiently.

Make numerical programming a priority. Technical computing deserves a really first-class modern language that’s focused on it. After a couple of decades, Python finally threw NumPy a bone and added the @ operator for infix matrix multiplication. Numerical computing is just not a priority for the Python core devs – and reasonably so, since that’s not the language’s primary focus. Julia, on the other hand, has been about numerical and scientific work from the start, so we care about usability and convenience in those areas. This is not a diss on Python in any way, it’s just a fact that numerical programming will never be a top priority for Python the way it is the top priority for Julia.

Have an expressive built-in language for talking about types. Every numerical computing library ends up needing to talk about types. At a minimum, you can’t do anything practical without typed arrays and being able to pass them to C and Fortran libraries like FFTW and BLAS – both of which require talking about types. As a result, there seems to be a Greenspun’s Tenth Rule analogue for numerical libraries, which I’ll posit as follows:

Any sufficiently complicated numerical library in a dynamic language, contains an ad-hoc, informally-specified, bug-ridden, slow implementation of a type system.

I just made this up, but it’s true – the SciPy ecosystem is littered with dozens of incompatible DSLs for talking about types with varying degrees of complexity and expressiveness. Python 3.5 has standardized on syntax for type hints, but from what I can tell, it still isn’t sufficient for what most numerical libraries need and probably never will be (priorities again) – it’s more designed for type checking in the presence of duck typing. If it quacks like a Float64 and walks like a Float64, is it a Float64? Not necessarily, if you really need your code to be efficient. Julia circumvents Greenspun’s Eleventh Rule (the one I just made up) by fully committing to having a type system and making the most of it for dispatch, types, error checking, code generation, and documentation.

Don’t make numeric types or operators special. What does it take to make “primitives” like Int and Float64 normal types and operators like + normal functions? This requirement leads you seemingly inexorably to dynamic multiple dispatch – which happily turns out to also be incredibly useful for many other things. Not least, neatly addressing the expression problem, which allows Julia libraries to share types and generic algorithms remarkably effectively. Multiple dispatch was a strange experiment at first, but it has worked out surprisingly well. I’m still regularly shocked that more languages haven’t used this as a paradigm before Julia. Dylan is the only language that’s taken multiple dispatch nearly as seriously as Julia. (And apparently Perl 6, from what I hear.)

Use modern technologies and design for performance from the start. These seem separate, but they’re not really. Language performance dies the death of a thousand cuts. No single design choice destroys performance – it’s an accumulation of little choices that kills it, even though each would individually be survivable. These choices are made in the context of technologies. Designs that are easy to implement with one technology may be hard to implement with another. So we should design for modern, high-performance technologies from the beginning.

Most dynamic languages were designed to be interpreted, and interpreters are relatively slow. So no one thought that much about speed when these languages were designed. Later, when you try to apply modern technologies like JIT and LLVM, choices that were made on the assumption that a language would be interpreted are often a big problem. For example, representing objects and scopes as dictionaries is a reasonable choice in an interpreter. From there, it’s perfectly natural to let people manipulate objects and scopes just like dictionaries – after all, that’s what they are. But making code fast requires making local variables and objects completely vanish – the fastest computation is the one you don’t do. But once you’ve committed to exposing dictionary semantics and people have written tons of code using those features, it’s too late – you won’t ever be able to effectively optimize that code.

Julia, on the other hand, has never had a full interpreter. When we started the project, we looked at compiler frameworks, picked LLVM, and started using it right away. If a feature wasn’t easy to generate code for, it was never added to the language in the first place. Often there are slight variations on features that make them vastly easier to generate code for but don’t really eliminate any expressiveness. eval that operates in local scopes? Impossible. eval that operates only at global scope but allows you to define new functions (which have local scopes)? No problem. A lot of Julia’s design fell directly out of making it easy to generate LLVM IR – in some ways, Julia is just a really great DSL for LLVM’s JIT. Of course, code generation frameworks aren’t that different, so Julia is really just a nice DSL for any code generator that can be invoked at runtime.

103 Likes