Dear all,
I wrote a new blog post on Julia. This time, a review of the use of trimming for Advent of Code so far.
It also contains a bunch of opinions / speculation / rambling about Julia and Julia development in general. Enjoy.
Dear all,
I wrote a new blog post on Julia. This time, a review of the use of trimming for Advent of Code so far.
It also contains a bunch of opinions / speculation / rambling about Julia and Julia development in general. Enjoy.
Thanks for sharing! Learned something new ![]()
Not the prettiest, or most readable error. It says
fatal: error thrown and no exception handler available.. I would guess that Julia’s normal exception handling is one of those critical components of Julia itself that unfortunately contains dynamic APIs, and therefore can’t be trimmed - probably, JuliaC has a separate, inferrible implementation of exceptions.
We can and maybe should fix this, try catch is perfectly inferable, it’s just that we don’t insert one around your main function like Julia normally does. But printing a stack trace properly is hard funnily enough. julia/base/client.jl at 3484331cb1925a67b7e08f706bd36d34791ae9d6 · JuliaLang/julia · GitHub for where it usually happens
Thanks for sharing! What a brutally honest and sincerely informative review of the trimming feature! Personally, I really empathize with this part:
I could work around the issue by wrapping the union in a struct, which would change the compiler’s heuristics about inference of unions. But requiring users to exploit internal compiler heuristics is, of course, unlearnable, brittle, and completely unreasonable.
To me, the core problems still lie in the current limitations of type inferability. It’s time to face the harsh reality: tracking return types under the current version of Julia’s type semantics has been proven unable to keep up with the increasingly divergent call graphs enabled by multiple dispatch.
If Julia still wants to be greedy about having both static compilation and dynamic interactivity, perhaps the most urgent thing on the roadmap is to improve the language-level capability for bounding inhomogeneous types. My personal wish is a formal interface syntax for abstract types, or at least a proper sum type that can bundle multiple parametric types while being invariant.
I think the near future of the language will have formal interfaces and a strict sublanguage. This is just what it has been building towards: write the simple code to get things work, but for core packages, then take the time to refactor towards a static sublanguage with more guarantees, lock it down and have it compile time checked.
Solving the two language problem isn’t about making any code work fast, it’s about making it so that there is a way to write a code that is very simple and easy, but then there’s a gradual process to get a very fast and statically inferable program. The key is that no rewrite is required: rewrites are brutal and always have issues. Instead, it’s about having a process whereby you can gradually change one line at a time, one function, and finally one package at a time, towards more guarantees. What is lacking right now is then the ability to lock it down in the end, to say okay, this is now in the strict mode, this well-defined acting part of the language, and now everything has compile-time errors and complete static guarantees.
Won’t this create a two-language problem, though?
We’d then have issues like “why does this code work here, but not here?” “Well, there are two kinds of Julia…”
That’s already kind of what happens with large codebases: we end up rewriting things with loops from the start, preallocating stuff, using views instead of slices, etc. I agree that the idea of strict mode possibly being applied at a project level is a bit scary, but maybe there are ways to activate it only for specific parts of the code? Even then, I feel like having it at the project level is fine too this happens in every language. Would you say there are “two” Rusts just because you can go fully unsafe or fully safe or “two” V because you can compile to c code or to native code.
As for the comparison with Python, in both cases it will be the same compiler here which is not the case for Jax. Strict mode will be handled at lowering if I understood correctly, so that’s fine. I also like the fact that code written in strict mode should always work in dynamic mode. I feel like that’s enough: you might want to start from strict code and add some dynamism for special cases, but when you take dynamic code and want to make it strict, it’s kind of obvious that some work will be needed. Webservers are done in static languages too harder but possible.
Finally, strict code doesn’t mean faster code, it may endup being as fast or worse in many cases, proof is we already beat C++ on many code. However, for debug, analyses, awairness, its not even close, static is just better on this. This may tell llvm stop doing whatever you want on what I call so that I’m able to follow you, in exchange I give you more info about what I meant, so yes it will have more info but if it could have done better without it we will loose perf.
Good post, the only thing I would add is that GPU compilation goes through an entirely separate path. In that sense, Julia is “3 languages in a trench coat”: static, dynamic, and GPU.
Technically no because it’s still one language with the same philosophy and semantics. Someone has called this the “1.5-language problem” before, but I never really bought into that idea. Every language has its contextual subsets and styles, and switching between those has different challenges from switching languages.
We can also throw in precompiled Julia because it has dynamic limitations but can use the runtime.
GPU is not in Base so I don’t think it works
otherwise every symbolic package is its own language
Not sure, I don’t really use Rust.
I definitely wouldn’t say this means there are two Vs because the language you write (V) stays the same. I think there’s a language called Haxe or something that can be transpiled to tons of languages, but the whole point is that the programmer simply writes Haxe (?), only one language, and then hands it off to transpilers that work their magic. The programmer only directly uses one language, so no N-language problem here.
That’s an implementation detail. Perhaps I misunderstand the N-language problem, but I think it means the programmer has to write code in two languages to accomplish stuff. Like Python is “slow”, so write performance-critical code in other languages like C/C++/Rust. For that, you need the relevant compilers and skill with these other languages. Here I’m talking about the languages you write: JAX isn’t really Python, strict Julia is theoretically not really Julia. I guess this is the point, because “regular” Julia is too dynamic (same with “regular” Python, by the way).
Yeah, this is an absolute must, of course. Otherwise there will literally be two different and incompatible languages within Julia.
Yep, it’s like async functions: you can’t await an async function from a regular one, so you must either make that function async too or have two different pieces of your program: one dealing with async stuff, the other doing plain compute. I think there’s a famous essay on the topic, having “colored functions” in the name.
BTW since “strict Julia” is purely hypothetical, I can’t really argue about this introducing an N-language problem because I don’t know what “strict Julia” could even look like. But it seems like a fun semi-philosophical consideration.
Actually, since we have:
…then this means that “strict Julia” could be very different from “dynamic Julia”, so much so that one could look at a package and tell whether it’s one or the other. That’s interesting to think about. I’m starting to feel like I’m becoming the “N-language-problem person” who takes every opportunity to question whether Julia really solves the N-language problem and what this problem even is…
Ok I see, I feel like it’s just better to have some kind of a 1.5 language problem (because you will never need to write wrapper from the static part to the dynamic since its the same code) than just keep stating julia is general purpose and have issues with real-time and embedded systems. So even if it add some issues like “why this code doesn’t work in strict mode” (which I really feel like won’t happen that much since this will be discouradge when not knowing what you do explicitly saying people they won’t win a lot using it without knowing why ) I think it is still better than not being able to deal with enormous fields in software engeneering.
Also going little by a little to strict mode without activating will be duable since strict mode code will work without it, so those can be considered as improvement until the final super trophee “strict mode activated”. Anyway, we have no idea how it will look yet so we can’t really state anything further.
I just realized this is a blog post anousement, so sorry for the tangent discussion, the blog is really great congrats !!!
All of SciML has been written in this form from basically the start and we just call that normal Julia. The only difference is that we are now asking for early error messages if we forget something
I don’t really think having a static dialect of Julia would re-introduce the two language problem. Of course it’s hypothetical since no such dialect exists - but I would argue that even if such a dialect would have quite different semantics, it still would be far from the two language problems.
Why? Because even a static/dynamic Julia split would suffer from almost none of the drawbacks that makes the two language problem really annoying:
@code_warntype, profiling, @trace_dispatch and what have you.In short, I don’t think the two language problem is really about just having two languages. It’s about all of the annoyances listed above, which a split Julia wouldn’t have.
Why do you want to have trimming?
I started out thinking that it would be great to integrate a Julia produced shared library it into the JVM, but apparently this won’t fly due to both runtimes using signals.
Reminds me of Common Lisp, where they added the option to “seal” functions. It prevents further redefinition and allows julia-style compilation to native code without runtime dispatches. Of course, it won’t help with dynamic Lisp code written 40 years ago.
Sounds like this:
Thanks for the interesting writeup. The one thing I disagree with is that
Among the many weaknesses caused by its hybrid nature is that Julia is hard to deploy.
I deploy Julia routinely in various computational environments, and it is trivial. This is something CI actions scripts on Github do every time you run them. It is a solved problem, and have various robust solutions, both for interactive (juliaup) and container workflows.
Sure, it is not instantaneous. And, compared to, say, a compiled C or Rust binary, you are moving a ton of bits. With nontrivial startup times.
But in a context where you are calculating something that itself has a nontrivial computational time, these things are negligible.
Would I want tiny, fast, precompiled Julia programs? Sure it would be nice, I would replace Bash scripts (which I hate writing, after 20 years of Bash I still run into weird corner cases) and similar with them. But it would be just icing on the cake.
We’re talking past each other. “Deploy” can mean different thing for different people. Does it need to run on machines other than the ones it was compiled on? Does it need to pass notarization? Small binaries? Multiple instancies? 0 allocations? CCallable libraries?