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.