Discussion on "Why I no longer recommend Julia" by Yuri Vishnevsky

That’s why I sticked to the Java, C#, C++ definition of OOP. Most people think of that flavor when talking about OOP. I’ve considered adding some footnote in the blog about that OOP is not strictly correctly implemented in modern languages, but that seemed to distract from the main story. By “correctly implemented”, I mean implemented according to the definition by the original creator of OOP. There is a nice talk on YouTube somewhere by the original creator. Maybe it was on a Smalltalk conference? He tells how everyone got OOP wrong. Can’t find it now, maybe you know the one

4 Likes

But… Then what’s the difference between types and classes? I thought classes where types with methods in them.

1 Like

All classes have a type of the same name, but not vice versa. That is, INTEGER is a built-in class, but a type describing an integer in the range of [0 10) is not a class.

Also no. The whole point of my post was to mention classes have nothing to do with methods. Multiple dispatch is not a new idea to Julia, after all.

2 Likes

Maybe this one in case others are interested.

3 Likes

One lives and one unlearns, apparently. Yesterday I knew the difference between a type and a class, and had a vague idea about what interfaces mean. Today, after reading a bit more about them, I have not the faintest clue about any of them.

Carry on.

21 Likes

I really wish the exact meanings of programming lingo remained the same across all languages and contexts, but it would be unfair to expect some primordial language theory to formally delineate every weirdly specific concept some future language comes up with.

1 Like

It does appear that assuming 1-based indexing is all over the place and isn’t always marked with require_one_based_indexing(), e.g. begin + n - 1 only works for n in 0:typemax(Int) if begin is 1.

I want to remark on an interesting complication for the task of hunting down 1-based assumptions in Base, StatsBase, and elsewhere. require_one_based_indexing() shows up in a lot of methods where there are >1 input arrays, and even if one were to rewrite them to start at firstindex instead of 1, there’s still the issue of what to do with the inputs’ offset indices. For example, take the addition of matrix A and matrix B, yielding a matrix C. Let’s say A is 0-based and B is 1-based. This isn’t one of those offset indices applications where aligning A[1,1] and B[1,1] makes any sense, so we basically do A.parent + B.parent. But what should C’s indices be? 0-based, 1-based?

I would argue that at the most, such methods could be sensibly relaxed to calling a hypothetical require_same_indices(), and that restriction makes it unreasonably clunky to use OffsetArrays. At least StaticArrays do not support a conventionally obvious interface method (setindex!), so you know to avoid mutating! methods; it’ll be harder to check if a method uses require_same_indices() in some really nested method call.

One thing about this discussion and the original blog post has been annoying me—the use of the phrase “correctness bug”. I view that phrase as a melodramatic and alarmist way of saying “bug”. Aren’t all bugs “correctness bugs”? Obviously code can have performance issues, but I hesitate to call performance issues “bugs”.

The cpython repository has 2,740 open issues with the label type-bug, which has the description “An unexpected behavior, bug, or error”.

Julia has a total of 3,383 open issues (Python has 6,773), but presumably not all of them are bugs. The bug label has not been applied as consistently in the Julia repo. Only 215 issues have the bug label, but there are probably more than 215 open bug issues for Julia.

So, the number of open bugs in Python and Julia is probably pretty comparable. Yuri’s blog post, and some of the comments in this thread, take an alarmist tone that implies that the Julia language is unreliable to use. Now I’m perfectly willing to admit that the Julia ecosystem probably has more bugs than major Python packages do, but I think that’s primarily because popular Python packages have many more users and many more developers.

31 Likes

Just for information, I also think this is a serious problem. I asked for this feature here:

which has an initial PR here:

8 Likes

I don’t think so. There’s a big difference between “Calling X with inputs Y produces an error” and “Calling X with inputs Y returns an incorrect result”. Rereading the blog post, the latter is what he calls a “correctness bug”.

Those bugs are scary. His point is that because types like Number or AbstractArray are not crisply-defined concepts at the language level, different packages can make different assumptions, and that leads to “correctness bugs” (i.e. incorrect results), perhaps in a way that doesn’t happen (as much?) in languages that don’t encourage/enable interoperability as much as Julia does.

25 Likes

I definitely agree correctness bugs are a big deal and very different from bugs where something throws where it shouldn’t or isn’t as fast as it should be.

Code is used to understand things and make decisions. If there’s a bug that compromises that understanding and e.g. invalidates an analysis for a paper, or a decision based on the results from the code, then that’s pretty bad. At work I use a label “correctness bug” on issue trackers to distinguish them from other bugs, and try to announce when they happen and are fixed so downstream consumers can re-run code. Luckily they are fairly rare compared to other bugs.

I don’t think these should be dismissed at all and agree with other commentators that we need an increased cultural emphasis on testing edge cases and communicating guarantees or lack thereof (e.g. interfaces, input checking/errors, assumptions documented).

16 Likes

but I hesitate to call performance issues “bugs”

In almost all numerical codes, bugs manifest as performance issues, especially when solving multiphysics problems. Any kind of iterative solution program performs poorly if there are bugs in the code. In C, they are mainly memory overruns, incorrect indexing (0 based vs 1 based). Integer overflow or underflow sometimes updates a memory location instead of the intended one. These are problematic when one computes norms which are used as stopping criteria for iterations - inner iterative solvers called by an outer non-linear iterative loop.

Since Julia competes with C/C++ in NA space, performance issues should be called bugs unless otherwise proven that the performance issues are inherent due to limitations on the available algorithms whose asymptotic time/space complexity is proven to have these limitations.

2 Likes

Julia was created to exploit synergies of composability.

Yes it’s possible to compose things incorrectly (and to get an incorrect result, instead of a bug)

No, Julia does NOT have a correctness problem, if used correctly

10 Likes

If you as a user train a model with a high-level library and silently get incorrect result because deep down in the internals there is a function that is not being AD’d correctly, it’s incredibly hard to debug that. It is not a fundamental problem, but one which is a consequence of the composability and no single entity being responsible for correctness. For example, the loglikelihood of some distribution may give incorrect gradients with some AD library that is stuck in an optimizer, while a different loglikelihood worked well. So you may change one aspect of your model and suddenly it does not work at all, or becomes 100x slower. It’s hard to say what was an incorrect use in this case, and the solution is not obvious without diving deep into the issue.

3 Likes

As a user (not as a package developer), how would you know if you are using Julia correctly?

2 Likes

For example, take the addition of matrix A and matrix B, yielding a matrix C. Let’s say A is 0-based and B is 1-based. This isn’t one of those offset indices applications where aligning A[1,1] and B[1,1] makes any sense, so we basically do A.parent + B.parent. But what should C’s indices be? 0-based, 1-based?

That’s an extremely dangerous way to think about the problem. Why does aligning indices not make sense? I expect (A + B)[i,j] = A[i,j] + B[i,j] (“location matters”). Indexing needs to be guided by a few axioms, the other key one being that a[indxs][j] = a[indxs[j]] implies that “the indices of the indices become the indices of the subset” (i.e., the indices of indxs become the indices of a[indxs]). We have good support for such axioms, except of course when people don’t implement them “properly.” Of course you can write a method sum_without_worrying_about_indices yourself, but that’s not what + should do.

The right answer to your question is, throw an error. And we do:

julia> A + B
ERROR: DimensionMismatch: dimensions must match: a has dims (Base.OneTo(3), Base.OneTo(4)), b has dims (OffsetArrays.IdOffsetRange(values=0:2, indices=0:2), OffsetArrays.IdOffsetRange(values=2:5, indices=2:5)), mismatch at 1
Stacktrace:
22 Likes

No-one can be sure they’re writing correct code.

If you write ’boring’ code, it’s more likely to be correct.

Those are intentionally pointed comments. Prob not always true, but often they are.

1 Like

I agree. The part that “doesn’t make sense” is aligning C[1,1] = A[1,1] + B[1,1] means that there’s a A[0,0] with no B[0,0]. One could force it to work by allocating extra elements so C[0,0] = A[0,0] and adding 2 NxN matrices makes a (N+Ma)x(N+Mb), and that doesn’t make sense to me either. As you said, a cheap check and throwing a DimensionMismatch error makes sense. Still seems like OffsetArrays are a bit more complicated to work with than other AbstractArrays, but I suppose if anybody deviates from 1-based indexing they’ll just need to be extra vigilant about dimension matching.

So any thoughts on matrix multiplication? Say 4x3 matrix X has indices (0:3, 1:3) and 3x2 matrix Y has indices (1:3, 2:3), do you think it’ll make sense for the 4x2 matrix Z to have indices (0:3, 2:3)? I can’t really think of any meaning for it, but if it’s disallowed then OffsetArrays must contend with more than just dimension mismatches (the 1:3 axes do match).

If I am composing things from 2 packages that were not designed to work together, I:

  1. test things out with a bunch of sanity checks (on simple problems I already know the answer to)
  2. study the underlying code for the functions I’m composing

I never just trust things will compose correctly… In any language

10 Likes

As I see it, many generic functions (morally) assume diagonal dispatch, i.e., require that arguments are of the same type. In case of adding matrices, this requires that the indices are from the same domain as addition requires that both arguments are elements from the same set/vector space etc. (Note that in Haskell this is often enforced in typeclasses, i.e., via type signatures a -> a -> a and the index is part of the type signature for array operations).
The case of matrix multiplication is different though, as this corresponds to composition of linear maps and the only requirement is indeed that the inner domains agree. As an extreme example, we could even have named indices, i.e. C[apples, peaches] = sum(A[apples, orange] * B[orange, peaches] for orange in oranges) where all indices range over valid values from their corresponding domains, e.g., apples = Fuji, Gala, Jonagold, ...

2 Likes