This is how I understand the “why Julia?” question. This was my round 2+ after I didn’t have an adequate answer written down the first time I did this workshop.
Julia essentially answers the Pyston question: how should a language be restricted and thought about so that way it still looks and acts dynamic, but in the end is very easy to extend and optimize? When you understand how Julia’s type system allows for JIT compilation to not just work, but work very well by specializing on exactly the functions its seeing, then you see what makes Julia special and how to apply this design to your own codes to get similar results.
I was recently giving a talk about DifferentialEquations.jl, at a part where I was highlighting the use of odd-number types (SymPy expressions, ArbFloats, ApproxFun
Funs) in the differential equation solvers. I got an immediate question from the audience along the lines of
I know from Python that if you objects inside of numerical methods / loops, it will immediately be very slow. And it’s difficult/impossible to get user defined objects in something like Numba. So while all of this stuff is really great for expanding what we can solve… won’t it be very slow?"
Of course I had to answer (along the lines of)
That’s a limitation of Python. Julia’s type system is specifically designed so that way user-defined types are just as fast as those which are “built-in”. In fact,
Float64is just a type which is defined inside Julia. You can, the same way Base was built, build up your own crazy numbers and arrays, have them be fast, and they will “just work” via dispatch. ApproxFun is probably coolest example: it’s type
Funis essentially a “Function in a function space”, but from Functional Analysis we know that those functions are just points (numbers) in this new space. So you can define
*, etc. between them… and then just stick that “number” in functions or libraries written for
Numbers and get out the solution to a PDE.
The key is understanding why the abstraction works. Because Julia recompiles what each of those operators mean, when you put in some complex type it essentially compiles the code you would have wanted to write. When you start to get the hang of this, your Julia code can become bizarrely abstract, and you can compose interesting type + generic function to get fast implementations with no work. A good example of this is SymPy.jl or SymEngine.jl. These implement symbolic mathematical expressions in Julia. When you first start using them, it seems odd that the documentation is so little. But then you understand that “oh,
/ is defined on a symbolic expression, and therefore to take the inverse of a matrix of SymEngine expressions, I can call Julia’s Base
inv function. Nobody ever needed to implement such a function because the way
inv is written only requires those dispatches.” And when you benchmark it, it’s super fast (it can sometimes be faster than built in SymPy expressions…). All of the “linear algebra fallbacks” will make matrices of SymPy/SymEngine expressions “just work”. You can string together fast algorithms using methods nobody ever wrote but the compiler makes for you from generic code. That’s something you don’t get with Python.
All of this “zero-cost abstraction” allows large software ecosystems like JuliaOpt, JuliaDiffEq, JuliaStats, JuliaML, etc. to be developed very fast, but have amazing flexibility and performance. At this point, I think we recommend Julia for its packages, since there are quite a few things which Julia packages can readily do that I wouldn’t know simple ways of doing in other languages.