Hello, i hope this is the right place to ask this and this horse hasnt been beaten to death. I am new to Julia and i love JIT compilation and multiple dispatch.
I recently read (and tried to understand) Yuri Vishnevsky`s Post about “Why I no longer recommend Julia”.
But no matter how much i try to understand or research it, i dont understand the root of those correctness issues. How exactly does Julias unique design (multiple dispatch, flexibility) lead to those bugs being more common? Is that even true or is it just that Julias libraries are young and not as rigorously tested? What is the root of that repeating discussion?
thanks
Welcome! Yes, there have been many long threads on this in the past. Here are two comments that I think speaks to your question at a high level, but there are many good responses there:
I will also add that there’s a wide range of dis/agreement with the various points and so this topic can often spark some lively debates.
It has, but if the earlier threads weren’t discoverable or didn’t answer your questions, that’s not your fault and you’re free to ask. Just don’t expect the conversation to go as long this time.
First, there are definitely libraries that are staffed by very disciplined and responsive developers that I wouldn’t turn anybody away from; you’re in as good hands as any. However, Julia as a whole has much fewer users and developers and in fewer domains than some languages, so it’s not surprising that those other languages have more reliable libraries for more purposes. This isn’t down to language design and is partially explained by youth, but it’s mainly resources and logistics. The silver lining is that people are working very hard to improve this situation, and it is.
Second, the bulk of the issues came down to the supertype AbstractArray
expanding its specification/interface. I know you’re new so I won’t explain all the language details, but suffice to say that a type has documented promises on what it’s like and what we’re allowed to do with it. For a while, AbstractArray
subtypes in Julia, especially the base Array
type, used 1-based indexing, so that’s what code was written for. However, a subtype of AbstractArray
could potentially use 0-based or any other linear range for indexing. A lot of code in the ecosystem was extended to accommodate those subtypes, but there were still holes that assumed 1-based indexing (not obvious if not directly indexing!) and would make sometimes silent mistakes, both in base Julia and pre-existing third-party packages. The package OffsetArray
s for X-based indexing exposed many of those holes, though that’s not really its fault. It’s worth pointing out that many of the cited issues were fixed by the time the blogpost was written, though it’s understandably frustrating to run into older code that retroactively broke a promise. These weren’t fringe packages either, these were well-respected and used in practice; 1-based indexing was just still so common in practice that the holes were tolerable. Again, it takes time and resources to patch these holes, which goes back to my first point, but this problem is not just down to resources.
Third, a few issues showed an expectation of composability that didn’t match reality. Again, you’re new to the language so I’m omitting the finer details, but composability is the ability of independent packages to work together. If you’ve used other programming languages before, you might notice that this is actually a very common feature, but multiple dispatch just makes it easier and more flexible in some ways. Thing is, composability doesn’t just happen naturally; not only do packages need to honor each other’s promises, their internal code had to uphold their own promises in new contexts. New compositions can either expose some very hidden broken promises that even rigorous tests couldn’t catch, or just show that a package could be a bit more flexible than it originally designed it to be. Formal, strictly decided interfaces were mentioned as a possible solution and many do want them for various benefits, but in my opinion, that won’t prevent this issue. No matter the language or how many formal restrictions it provides, writing a broken promise into internal code is just as easy and usually not as obvious as 1-based indexing of AbstractArray
s. These and other bugs are routinely discovered and patched, and it never stopped any other language with composability from being successful, often more so than Julia. Incompatible packages often error immediately because they don’t have the necessary types for the interfaces; some bigger dynamic languages don’t even have that safeguard.
There are a few more things, but that should cover most of the discussion and it’s what’d you have to care about as a new user.
I debated whether or not to comment, as this topic indeed has seen a lot of mileage here. However, I do think there is a big factor for the persistence of such discussions that I don’t tend see emphasized (much) in previous threads, which usually seem to focus on the consequences of overly-broad method signatures, ambiguous interfaces, and flexible compositions.
And that factor is, in my view, the kinds of bugs that do occasionally pop up can be things that feel very fundamental / should be bullet-proof, and they also are often seemingly-random regressions of something that worked for a long time. some select examples of these but there are more
- 1.9.2 breaks
map!(|, ...)
onBitVector
map!
is very broken with inputs of heterogenous sizes- a simple function of
mod
andmax
returnsNaN
incorrectly- to be fair, this one never hit a public release
- an error is not thrown when a seemingly-unrelated loop is removed
- an
UndefVarError
is not thrown when it should be - another function returns the wrong value instead of erroring
I wonder if a blog post or Q&A section on the Julia-lang website about Julia 2.0 and Yuri’s blog post might reduce the number of message threads on these recurring topics.
For example, the Go developers wrote this about Go 2.0:
There will not be a Go 2 that breaks Go 1 programs. Instead, we are going to double down on compatibility, which is far more valuable than any possible break with the past. In fact, we believe that prioritizing compatibility was the most important design decision we made for Go 1.
In addition to the topics and discussions here, there are indeed multiple blogposts on the topic, including:
- Why we still recommend Julia by Matthijs Cox and Jorge Viera
- Why I still recommend Julia (for Data Science) by Rik Huijzer
Maybe Julia’s FAQ can address the appropriate expectations for a v2, but it’s not appropriate to officially single out one person’s opinion at one point in time. Yuri wasn’t the only person who publicly declared Julia wasn’t worth trying, and it’s one of the better opinions because despite whatever disagreements persistent users may have over the expectations or conclusion, there were constructive criticisms that developers have been taking seriously before and after the post. Also, whether Julia or a third party library of interest has adequately addressed any of those criticisms or generally reached reliability should be discussed in real-time, not be left to an overgeneralizing statement that is likely to become obsolete within months.
I feel there is almost this tendency that a language cannot achieve all three: genericity, accessibility, safety. This is due to generic code having assumptions, and the assumptions are hard to state without some sort of abstract algebra, which is inaccessible. So, with accessibility, you either are not generic or not safe. However, not being generic is not an option. There are simply too many combinations of operations you want to do on something and the things you want to operate on. Out of the systems that can automatically generate methods, Julia is as safe as possible. Using generative AI is the other option, and is even riskier.
Sorry to be so blunt, but this sounds entirely preventable: How can we expand the ecosystem like that, and not expect everything to break?
Also: As far as I understand causes the composition of certain packages error.
Is it possible to automate CI, that checks whatever or not that may be the case? Are there any saveguards in place, to prevent this from happening in an automated fashion?
How do other languages solve that?
I found many such “you can only have two out of three” scenarios, they are almost a cultural staple.
And again and again, do I find that the culprint behind this is the overextension on the two prominent features:
The solution here seems to relax a bit on genericity, in areas where it doesn’t matter in practice.
I would argue, that the genericity to combine packages who dont work with each other is an anti feature, so reducing the capability to do so, is desirable.
Like it used to be with the well tempered piano, that caused Bach to write his just named work.
In previous times, pianos would sound only fine on one single pitch.
A piano maker discovered a way to tune the instruments in such a way, that made the whole range sound fine.
The catch about this is, that the pitch that sounded perfect before, sounded a little bit worse than before, just a tiny amount.
Perfect is the enemy of the good, and hyper fixating on one feature is just a guaranteed way to fail at the other fields.
So I assume when we automate compatibility checks, there will be a few false negatives and a few false positives.
And it will still be worth it.
Things are already automated, but that doesn’t catch problems if tests don’t tell it how, and tests are harder than people think. Not all of the mentioned bugs are immediately apparent when people tried making two packages work together, some of them only happened if they stumbled into niche situations or inherently bad practices. Some problems like the latter aren’t just something to fix, it’s something to debate; do you add unwanted and often unnecessary overheads to evade or stop rare mistakes, or does the documentation inform users how to handle it when they need to? I agree that prevention and safeguards are good, but getting everything right forever on the first try is impossible. A realistic expectation is useful code that is better understood as more people use it in more contexts and provide feedback, which is exactly what’s happening. In my opinion, the argument that there is something missing in Julia or the culture that will prevent or reduce bugs (lumped into the “correctness problem” concept) is not substantiated by the existence of bugs, and none of the loosely suggested changes to interfaces can do anything about many of the listed bugs. It just seems like we need more people and resources to get the work done faster, there’s no easy way out.
Note that none of this contradicts Yuri’s decision to stop recommending Julia or various parts of its ecosystem. If your work is routinely running into bugs and you don’t have the resources to fix them or wait for fixes, you shouldn’t use the software, Julia or not. Contrary to the repeated argument that persistent users and developers showing any confidence in Julia means that issues are minimized and ignored, people are in fact routinely informed about shortcomings (links to Github “issues”, not Github “everything-is-fine-actually”) and more established alternatives outside of Julia, if not outright encouraged to use them instead.
The packages don’t necessarily don’t want to not work with each others. They might simply not know each others. For example, I once used Forwarddiff on an opensimplex noise from a library and it worked fine.
Moreover, it is nontrivial at all to make functions safe with overloading or multiple dispatch. Semigroup property is a property useful in some data structures, SIMD map-reduce, and maybe a bunch of other things. However, how can you guarantee such property if the implementation could be overridden? (A+B) + C = A+(B+C) might be true for integers, but if you implemented a new int type and it has bugs or peculiarity, would it hold? And Semigroup property is more nuanced than you might think. For some use cases, it holds well enough for floating point numbers to be used. For those dealing with precision arithmetic, it might not. One problem with overriding and multiple dispatch is that some combinations of argument types can bring the output type to another type with no information on the argument type and unknown properties. For example, float32 can be losslessly converted to float64. The Julia type system sees just 2 types abstract float.
Thanks for clearing that up!
I also think that checking generic programs is too difficult and a significant burden for programmers. However, if there’s a way to check specific program instances, this would be sufficient to avoid many errors. I mentioned these points in my Medium article .
Specifically, library authors could manually mark certain function instantiations to indicate that these instances have been relatively safely tested or proven. For example:
# a.jl, some generic array
f(x::AbstractArray) = …
# b.jl, some instances
@instance begin
f(x::Vector{T}) where T
f(x::Matrix{Float32})
end
Then, when calling these functions, if a specialization is not on this list, a warning would be issued. Of course, users could choose to ignore these warnings. This approach not only reduces the burden on library maintainers and users but also facilitates precompilation and testing.