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

Just me 2 cents thinking about the criticism. Not sure how much useful I have to add to this, but I feel Yuri may not do a proper apples to oranges comparison. Here is my hypothesis: Many Julia libraries and what you can achieve by combining them is extremely powerful relative to the man-hours that went into making them. I suspect when quality of Julia libraries is compared it is against solution which require far more manpower to build.

Yes, combining library A, B, C creates a number of permutations which are hard to test, but it also gives a lot of functionality relative to the effort that went into making these libraries. As I remark on in my story below I think the Apache Arrow project is illustrative. Correct me if I am wrong but that was the work of one guy in Julia. While dozens of C++ guys did the same. The Julia solution has to have a lot of bugs to be considered a bad tradeoff compared to the C++ solution.

As an old school C++ developer I never combined and used libraries so frequently and with such ease as in Julia, or any other language I have used. Our ability to combine and use more libraries give us more power but also expose us to potential for more bugs. I don’t think it is fair to say this power isn’t worth it or that we cannot overcome the problems.

If people can build massive systems with C, COBOLT, JavaScript etc which are of high quality then I am in no doubt that it can be done with Julia as well. I have faith in the future of Julia.

35 Likes

Just a small follow up on combining libraries: To me this is one of the killer features of Julia. Yet, compared to other languages there can be some gotchas in Julia.
Python: When using an AD kernel for a specific tensor operation, I can be sure that someone had a look at its code as it had to be written explicitly. In Julia I cannot, the specific combination might have been written by the compiler!
C++/Haskell: I pick those here as these allow similar generic programming. Yet, combining functionalities is more constrained by the type system in these languages. What is nice and reassuring in Haskell are the laws stated for each typeclass (concepts in C++ serve a similar purpose). They specify exactly what I can and cannot assume about a type and its operations, i.e., my generic code should just work for any type obeying these laws. As soon as I require any additional properties, e.g., commutativity instead of the stated associativity, I’m on my own and all bets are off. In Julia, I’m often not sure about what exactly I can/cannot assume for an interface. Maybe laws would make a nice addition either in documentation or even better as executable property tests (a la quickcheck).
In any case, having the ability to freely combine code from different libraries provides a huge kickstart for Julia. Just compare how much time/people were needed to provide the same amount of functionality in other languages, e.g., AD toolboxes for Python. Yet, guidance and assurance on the correctness of combined code might be improved …

12 Likes

My understanding is that Haskell typeclasses specify the above in type space. Which is somewhat useful, but still falls short of a complete contract.

To be concrete, consider

struct MyVector{T} <: AbstractVector{T}
    contents::Vector{T}
end

Base.getindex(v::MyVector, ix) = v.contents[ix]

Base.size(v::MyVector) = size(v.contents)

function Base.setindex!(v::MyVector, value, ix)
    if randn() ≤ 0.5
        @warn "I don't feel like setting the value at the moment"
    else
        v.contents[ix] = value
    end
    value
end

which fulfills every contract we can imagine in type space — a formal interface spec would probably would not find fault with this.

But it is still a stupid implementation, and encountering something similar in a package developed for practical purposes, I would consider it a bug.

Whether it is worth the extra complication for a language is a matter of opinion. The revealed preference of Julia devs seems to be that it is not a top priority at the moment, which I agree with.

6 Likes

Quite off topic, but this is only true if your type space does not have purity modelling, which Haskell iirc does have. So the path throwing an error taints the method, resulting in a compiler error due to not conforming to the expected interface, should that require purity.

Julia does model these things internally to some extent (or at least is starting too) with the up-and-coming effects system, but that’s a very orthogonal thing to formally specifying and checking interface compliance.

1 Like

True, the example can be misinterpreted. Consider instead something like

function Base.setindex!(v::MyVector, value, ix)
    if value ≤ 0.5
        v.contents[ix] = value
    end
    value
end

The point is that contracts about types catch some bugs, but are far from being a solution to the majority of bugs.

IMO their importance is overstated — programs that compile in languages that enforce these things still can and do have plenty of bugs. At the same time, this kind of formal interface spec complicates both the language and code written in it, and complexity itself can hide bugs.

But, again, I understand that some people like this, and there have been whole languages designed around the concept. It’s just that Julia, at the moment, isn’t one of them, and personally I don’t see a compelling reason to change this.

Instead, I think that most interfaces should have their own test suite, as suggested above.

15 Likes

Sorry, I should have been more precise here:
The typeclass itself specifies just the required types for each operation/function and is checked by the Haskell compiler … your examples above (except maybe for the exception) would satisfy those.
The laws are additional algebraic properties that the operations (morally) need to satisfy in order for an implementation to be correct, e.g., the Functor typeclass (generalizing map to arbitrary containers) has the two laws:

  1. fmap id = id
  2. fmap (g . h) = (fmap g) . (fmap h)

These also apply at the value level and are not checked by the Haskell compiler, i.e., they are just documented and should also be part of a test suite as suggested above. They do constrain possible implementations though, e.g., the first law would disallow the following implementation

fmap(f, v::AbstractVector) = f.(reverse(v))

even though it is typed correctly.

For your example, a property such as

getindex(setindex!(container, value, idx), idx) == value

should probably hold and be documented/tested.

6 Likes

100% agree. For example, the OffsetArrays problem is not one of missing formal interface definitions as far as I can tell. The “flaw” is that people writing algorithms assuming arrays start counting at 1 and harcoding that number in a variety of ways - many not captured by the interfaces at all - because it is much easier and what 99.99% of julia arrays start at 1. I don’t know fortran’s ecosystem especially well but I would be shocked if you can pick a random chunk of code from the internet (e.g. Burkardt’s wepage or TOMS or wherever people get code) and expect it to work with non-standard indices without reading the source.

What more formal interfaces would do is change the way that dispatching works. But it is unclear to me if Julia has the same problems or if something like traits would be better in Jula 2.x or 3.x etc.

C++ had a miserable time in the standardization of concepts (and eventually scaled them back last minute to the point that they help with static dispatching but had none of the fancy features that had previously been discussed). But C++ also had enormous hacks with SFINAE etc. which even scaled back concepts immediately helped with, and a stark difference between runtime and dynamic dispatching. Julia is a totally different model.

3 Likes

Lost in this discussion is that OffsetArrays offers a view with no offsets
OffsetArrays.no_offset_view which can be used to call functions that don’t support offset arrays.

10 Likes

I partially agree/disagree. When you mean with formal specification that this has to checked at compile time then I agree that this is not a requirement. On the other hand, when writing generic code it is important to know which properties of a type you can and cannot rely on (that is why I mentioned the laws in Haskell which are actually not checked by the compiler). In the end, generic code has to work across different concrete types, i.e., based on more abstract properties. Thus, you are right that code is “flawed” if it states AbstractArray in its signature and then assumes that indexing starts at 1 – as this property is nowhere mentioned in the interface for abstract arrays. In Haskell I would actually expect that code

  • stating Ix (the typeclass for abstract indexing) in its signature works correctly if passed an array with custom indexing
  • stating Functor in its signature works for any value from a type in that typeclass – no matter if you have a good intuition about how some particular instance is implemented or not.
2 Likes

Bugs are one thing. But it would be nice if the compiler told me which methods I forgot or didn’t know to implement.

2 Likes

For sure. I know where you are coming from.

I think the issue here is a community one (which is why I brought up that it would not have fixed the OffsetArrays thing in particular). Most Julia users do not have the expertise of haskell, CLOS, or C++ template developers, and I think the places where people are getting stung by the language tend to be more on ecosystem than the language itself. For me at least, I think that something like traits would solve immediate problems with dispatching. Beyond that, given how julia is used and who tends to write the code, I am concerned that making things more fancy with formal generic programming specifications might otherwise drive users away. The code already looks complicated to someone coming from python or matlab. Concepts in C++ solved a pressing need. Here I think the benefits of formalizing interfaces are less clear, even if it would be nice to have as an option.

5 Likes

I agree that it is very useful to be able to check if you implemented an interface correctly.

But I don’t think that the compiler is the right tool for this. Compilation may not even happen until a method with the relevant type signature is needed, so you may just learn about the mistake at runtime.

I think that a test suite for an interface is preferable for the following reasons:

  1. It can be incorporated into tests, with all the relevant costs (runtime, code complexity) happening during CI.

  2. the details for defining test suites for interfaces could be hashed out and improved very rapidly in a package (as mentioned above, such packages already exist), compared to the feedback cycle of Julia releases.

  3. the test suite could provide detailed reporting in a user-friendly format about what is broken, and also test at various levels of granularity (methods exist, invariants satisfied, etc)

12 Likes

That is probably very useful, but even just a function:

> InteractiveUtils.required_methods(AbstractArray)
"getindex"
"setindex!"
⋮

would help a lot.

6 Likes

The problem is that even simple interfaces do not translate well to a list of methods to have.

Consider eg

julia> r = 1:2
1:2

julia> r isa AbstractArray
true

julia> r[1] = 3 # gets you into trouble

even though for this particular case setindex! is defined, so you get a nice error message, but generally it is not needed for immutable arrays (eg StaticArrays.SArray does not have it).

4 Likes

But, how do you find out how to implement the AbstractArray interface? I feel a bit like I’m grasping at shadows.

1 Like

The manual is currently your best bet, but since it doesn’t even talk about setindex! being optional, so it is just quietly assumed. Cf

https://github.com/JuliaLang/julia/issues/21869

The bottom line is that unfortunately, interfaces are very hard to get right, both at the design and the implementation phase. Corner cases always surface with sufficient complexity.

FWIW, I think that the ideal solution to the interface problem would be

  1. every package that has an interface that is meant to be extended and is part of the API documents it,

  2. and provides a test suite for types implementing it.

4 Likes

Just today I stumbled upon a (apparently known) bug that relates to this long thread:

using ModelingToolkit
aa = [:a, :b, :c]
[en for en in enumerate(aa) if en[1] in (:a, :b)]

This results (in IJulia) in a long stack trace:

Stacktrace:
  [1] extract_gens(B::Vector{Union{}})
    @ AbstractAlgebra.Generic ~/.julia/packages/AbstractAlgebra/nmiq9/src/generic/Ideal.jl:1287
  [2] show(io::IOContext{IOBuffer}, B::Vector{Union{}})
    @ AbstractAlgebra.Generic ~/.julia/packages/AbstractAlgebra/nmiq9/src/generic/Ideal.jl:243
  [3] show(io::IOContext{IOBuffer}, #unused#::MIME{Symbol("text/plain")}, X::Vector{Union{}})

So, the error relates to show(), which… fine. However, the actual problem seems to be related to the AbstractAlgebra package, which I didn’t even know was even installed.

As @jakobnissen explains:

This is an issue with the package AbstractAlgebra , that pirated Base.show(::IO, ::Vector{<:Foo}) , thus overriding show for Vector{Union{}} It has already been fixed in AbstractAlgebra.

Which is great, but it is concerning that a random package can cause code that has nothing to do with it to crash.

Note that in Python, which is a very flexible language, it’s also possible to cause such a mess with some effort, which doesn’t absolve Julia.

I’m not sure what’s the right solution for this problem is…

DD

IMO, this is an unrelated issue. Julia allows you to overwrite methods in Base Julia, which can totally wreck Julia itself, e.g.

julia> Base.:+(::Int, ::Int) = nothing

fatal: error thrown and no exception handler available.

That’s an unavoidable consequence of allowing users to extend Base methods.
Even if it was not massively breaking to disallow people to overwrite Base methods (which it is), we would not want to do so, because it’s useful.

It’s also rather easy to avoid breaking Julia this way - just don’t do type piracy. That’s what AbstractAlgebra did.

7 Likes

I understand that. However, this does mean Julia can really surprise you. This is a real problem if we want reliable, production-level code.

One of the less-debated aspects of the entire discussion are the intellectual merits of the options.

I believe that MATLAB changed the way people think about scientific computing by putting interactivity, linear algebra, and ready, “good enough” solutions at the center. By comparison, while numpy, scipy, and matplotlib are impressive and have been massively impactful, their intellectual contributions have been minor at best; indeed, they mostly confirm Cleve Moler’s original vision (while sometimes improving on the implementation, to be clear). I am much less familiar with the history of data science, but arguably data frames in R played an analogous trailblazing role there.

Multiple dispatch and discovered composability are the first deeply interesting new developments in the computing aspect of scientific computing that I’ve seen since scientific Python ascended. In the few Julia packages I’ve written, it seems like dispatch and the type system make a lot of things click into place with greater clarity than I’ve ever gotten from, say, OOP. I bet others feel similarly about metaprogramming.

While the utilitarian arguments are important and will remain primary, those of us who are academics should stay mindful of Julia’s still-unclear potential to be evolutionary, if not revolutionary. In that spirit, I think it’s important to incorporate some Julia at least at the PhD level so that it can be part of the conversation. Indeed, some of those corner cases are likely to be the basis of interesting PhD projects!

33 Likes