Background (what I learned trying to make a language even though it didn't work out due to unforeseen difficulty my naive mind didn't see coming.)
I’m back to Julia for a bit. It is still a language that does wonders anyway, even though I realize it has cursed edges.
I even thought that programming languages were simpler than they actually are, and wondered why some features like XYZ were not implemented. In some ways, it could be, but it is much easier to make a programming language that compiles your code than to make a robust language that robustly compiles the code of programmers who haven’t read through the whole compiler. Julia does a decent job, as much as I run into the rough edges of the programs.
But now, I started to understand the important thing behind fast dynamic language, staging. If you could stage the interpreter, you might be able to jit-specialize it to run as if it were compiled, for example. So, without further ado, let’s proceed to the question.
It took me like a year to clarify what is intuitively wrong with Julia, but I want to understand why it was designed this way.
So, the key to performance in a dynamic language is staging. Runtime code generation, dynamic compilation, and so on are normally slow unless you stage them carefully so you don’t end up generating/interpreting code in the hot loop. Julia does make use of a lot of these.
However, Julia has AT LEAST 3 staging mechanisms, and they don’t always interact with one another really well.
- Macro expansion: happens at syntax time only. Sees no argument types.
- Type inference: happens when a function with known argument types is put in. If an unknown argument type is given, the function returns to dynamic dispatch.
- Generated functions: Functions with implementations generated during compilation. This is type-level metaprogramming, but AFAIK, the method table is frozen at generation time, and the world age doesn’t advance.
- Evals: Dynamically evaluate Julia code at the top level, but the world age is only updated once the function reaches the top level; it does happen if you don’t wrap everything in a function or if the evals were called in a setup before “main”.
- Packages: Some packages probably add their own ways of staged evaluation.
Now, each of these has its own distinct staged information, invocation time, invalidation rule, and generated precomputed/compiled result. They don’t always interact well with one another. (For example, macros can’t see the argument types; specializations can only dispatch by types, etc.)
Now, staging (and staged compilation) is important in fast dynamic languages, but for that to be used well, I think you need a way to manage the stages and not introduce many disparate staging mechanisms that overlap, but with each having a vastly different rule. That would be managing many systems at once.
But well, for some reason, despite me flagging this as a flaw, Julia still works well for many tasks, so maybe there is a reason I couldn’t see.
Why does Julia need so many distinct staging systems?