Rust vs Julia language

totally agree. Though I'd say that focus on high performance and high quality math packages in Rust is a net gain... there is already nalgebra, and I think it would be great to see more statistics and probability... though these maths are pretty popular today due to machine learning, they have very practical use in other domains.

as an addendum, I myself find that I'm usually overall more productive in something like Go than Ruby/Python. As I gain proficiency in Rust I'm curious to see how this pans out for me... but I expect that I'll be just as productive in Rust than i would be in something like Julia ... so I don't necessarily advocate the claim that languages are objectively more "productive" than others... I'm just trying to present some of the talking points of why someone would chose a data science project in Julia over python and/or c++

The reuse of code is something that shows much promise with Rust over C/C++. The embedded space is still using C++ because there's nothing that solves their needs as of yet (except Rust). The productivity enhancements for teams of coders is something Rust is presumably rather good at. It's quite improbable that a team of coders will spend years writing Julia code on the same application - unless they decide to do a general-purpose application which would be interesting to see. Rust is a general-purpose language though, so when teams get involved on large apps, Rust will likely win out over Julia. It's theoretically possible that Julia gets tons of general purpose libraries over time as well, but I don't see that as a good thing because it would mean Julia becomes yet another non-specialist programming language.

Julia would thus be more productive for data science, Rust would be more productive on general-purpose apps. Both are for reasons of library support, not inherent design weaknesses or strengths. But, Rust will always have the advantage and disadvantage of direct memory control, and memory control is something I hold in very high regard for my use cases.

I do think Rust can play a role in inferencing and deployment of ML, hence why good quality fundamental ML libraries would be a good thing for Rust. Academics can find some value from such libraries too if done well. I would, however, be sad to see Rust try to compete with Julia because there are so many languages trying to compete on features instead of building on their own strengths and working complementary to other languages.

2 Likes

for sure!

Do you have more info about those promises that Pony doesn't delivery?

The Why Pony? page lists a bunch of really great stuff. Quora gives reasons why development halted. But, one has to wonder how much work it would have taken to meet the statements in that Why Pony page.

I didn't know Pony failed so sadly. The complexity cost to use Pony type system seems high, higher than Rust borrowck, despite Pony GC could decrease Pony usage complexity a bit. I hope language design will go on looking for type system solutions to current programming problems.

As was already been mentioned, there are two factors at which Rust currently can not compete with Julia and which are important for data science:

  1. Lack of interactive mode, which is important for exploratory programming.
  2. Focus on matrix/linear algebra operations, which makes Julia significantly easier to write and read in this particular domain.

I would like to comment on the second one: I wonder if we can make Rust catch up Julia implementing linear algebra DSL via procedural macros. Right no by looking at example from OP link it's clear that it will be hard to win over data scientists right now:

// Julia
P = [a b  c d]
Q = [a b; c d]
// Rust
let p = { // P = [a b c d]
    let mut p = Array2::<f64>::zeros((n, 4 * n));
    let n = n as isize;
    p.slice_mut(s![.., 0..n]).assign(&a);
    p.slice_mut(s![.., n..2*n]).assign(&b);
    p.slice_mut(s![.., 2*n..3*n]).assign(&c);
    p.slice_mut(s![.., 3*n..4*n]).assign(&d);
    p
};
let q = { // Q = [a b ; c d]
    let mut q = Array2::<f64>::zeros((2 * n, 2 * n));
    let n = n as isize;
    q.slice_mut(s![0..n, 0..n]).assign(&a);
    q.slice_mut(s![0..n, n..2*n]).assign(&b);
    q.slice_mut(s![n..2*n, 0..n]).assign(&c);
    q.slice_mut(s![n..2*n, n..2*n]).assign(&d);
    q
};

Of course writing such DSL will not be easy (essentially you will need to write translator from Julia-like language to Rust, with human-friendly errors and such), and some people view existence of DSLs in a language quite negatively, but I think this field is worth exploring.

1 Like

This was mentioned only indirectly by previous responses, but I'll make the case here. Rust is fantastic for bare metal coding on embedded systems, and at a higher level for operating system kernels. (It beats the pants off of C and C++ in many areas, particularly the crates ecosystem.)

This is a space where I don't expect to see Julia ever realistically approaching. Nor any heavy-runtime language, for that matter. While many of these JIT/GC languages can have great runtime performance, they often pay for it with memory. And memory comes at a premium with most embedded systems, especially IoT devices.

What I do like about Julia (and Haskell, and Scala, and C#, and Swift, etc.) is that these languages are learning from one another and loaning very similar features, in many respects. Even if Julia isn't a great fit for writing a kernel, some of the knowledge is transferable to Rust when you need to get your hands dirty with low-level code.

3 Likes

I've used both Rust and Julia. I've been using Julia more for my actual work, while I've been following and doing hobby programming in Rust for a few years since before 1.0.

Making things short, Rust code is generally easier to refactor thanks to its good compiler error messages and has safer shared-memory concurrency, while Julia is much more productive to write thanks to being GCed instead of using ownership, and has much stronger support for generic programming (parametric + ad hoc polymorphism) if you aren't using Rust nightly features like impl specialization. It is also lighter on boilerplate and incidental complexity than Rust thanks to not having to deal with dummy trait impls, lifetimes, or ownership, although Rust's ergonomics initiative might partially alleviate this.

Julia functions are more open for extension than Rust ones since every function essentially defines the equivalent of a Rust trait that can be implemented on new types, while Rust's functions have better parametricity to the point where you can sometimes prove free theorems. Rust codebases generally require much more advance planning to write, while Julia tends to be nicer for more bottom-up programming where you gradually extend the domain of a function with additional definitions, for example by defining the factorial function first for integer values, and then extending it to arbitrary real numbers with an implementation of the gamma function.

Julia takes things further than Rust in terms of hygienic macro metaprogramming. Writing macros is somewhat easier, but more importantly, Julia has generated functions where the body of a function's source code can be made to depend on the types of its inputs. This is very powerful and can allows for some amazingly generic code.

Julia has a rational number type in its numeric tower like most Lisps. I'd love to see this in Rust. In general it manages to have a full numeric tower with promotion rules, without needing to encode that into the language standard. This is the key that makes it so much nicer for matrix operations. Rust sort of tries to do something similar with ops traits, but they are significantly less flexible due to Rust's strict rules for trait impls and lack of trait specialization on stable.

As for claims of being "fast as C", it is generally going to be faster than C for any large project beyond simple microbenchmarks, much like how C++ or Rust are typically faster than C for large projects. C doesn't scale well to any problem where you need abstraction, since everything tends to end up behind void pointers and function pointers/vtables when you need abstraction in larger C codebases. Garbage collection is also a complete non-issue. In the cases where you can avoid allocation, you can do so in Julia just fine.

In anything performance-sensitive, the GC is used in the cases where you would have used something like Rc or Arc in Rust, and using a GC is vastly better for performance than refcounts in those cases it if you don't need fine control over latency. You still lack complete control over that in Rust when using arc in multithreaded systems since threads race to decrement refcounts in Arc and the thread that decrements last gets burdened with calling the destructor, which can take an arbitrarily long time when freeing a big tree. If an arena or pool-based strategy can be used, both languages can use the same trick.

In cases where you don't need tree or graph data structures it's easy to avoid allocations completely so that the GC never needs to run and thus is a non-issue for performance. The advantage that Rust has for pause times is being able to prove that no allocations happen, while performance-sensitive Julia code tends to rely on the compiler's type inference and escape analysis to ensure that everything is stack allocated and this is generally checked by benchmarking in the REPL.

I'd be more inclined to use Rust for projects that need very strong safety guarentees or where I know that I'll encounter a lot of concurrency issues, while I'd be more inclined to use Julia for any project where I need to be able to get working code in a reasonably short timeframe or if it's scientific code. Failure to deliver a solution in a reasonable amount of time is probably more likely in Rust due to it having an extremely opinionated ownership model, while Rust solutions are more likely to "just work" if you can actually model the problem with it in a way that is reasonably natural. Anything with cyclic graphs is something that I'd be more inclined to do in a GCed language.

Cross-platform GUI's are also much, much more straightforward in Julia. Making a GUI with Gtk.jl is fully straightforward and there are libraries to wrap it into a higher level interface. By comparison, trying to use Gtk.rs has been extremely painful for me, in the sense that developing on a windows machine is extremely annoying to do, and so is deploying a cross-compiled solution from a linux machine to windows machines. Deployment of Rust code with non-rust dependencies in general is extremely painful, while Julia does cross-platform deployment more easily imho since you distribute the source code and you can have the package manager install any required dependencies on the target machine, with the only caveat being that Julia has to be installed on the target machine.

8 Likes

I actually always have a good time when I do this in rust, because the algebraic data types are so powerful. By the time I have defined my types, I basically have a prototype, and writing the algorithms feels like it naturally follows.

I would have thought this was undesirable. It's not good to have a function that behaves differently when its arguments have different types. You should have different functions.

I would have thought this was undesirable. It’s not good to have a function that behaves differently when its arguments have different types. You should have different functions.

Julia has parametric dynamic types which can have runtime values as parameters. Rust doesn't have dependent types and is just about to get const generics. Taking const generics as a simple example, consider a multidimensional array where the type contains the dimension of the array. You might want to iterate over it in a way that depends on the dimension of the array, in a way that is just as fast as if you hand-wrote the code.

In particular, there is a macro called nloops which expands something like this

@nloops 3 i A begin
    s += @nref 3 A i
end

to this:

for i_3 = 1:size(A,3)
    for i_2 = 1:size(A,2)
        for i_1 = 1:size(A,1)
            s += A[i_1,i_2,i_3]
        end
    end
end

Which allows you to write a function generic over the dimension of its array argument like this:

@generated function mysum(A::Array{T,N}) where {T,N}
    quote
        s = zero(T)
        @nloops $N i A begin
            s += @nref $N A i
        end
        s
    end
end

In Rust this would correspond to having n-dimensional arrays where the dimension is a const generic parameter, where a trait with a mysum method is implemented for every array dimension needed, with macros that expand to the correct numbers of loops for the dimension.

3 Likes

Agreed. It feels like there are so many application developers on the web and, now lately data scientists as well, that the number of users and the ubiquity of deployment of software is heavily skewed. Meaning that, while app and data science programming languages are spoken about so much, systems programming languages are relatively quiet, but yet make up such an important part of those same languages.

Some Julia code is written in C, Python interpreter is coded in C, LLVM is written in C++, Go is written in C and C++, Windows is written in C, Mac is written in C, Linux is written in C. Virtually any new programming language has parts of it written in C, and all the GC languages use C at some point in the process.

The point is that, to a large extent it really doesn't matter that most specific areas of application will have another language that functions better than Rust for that speciality. In the end, Rust can go where none of those languages it gets compared with can go. And that makes any comparison with it apples to oranges.

The one criteria for Rust will be whether it can displace C/C++, because that is the one language which has code that 100% of users will get into contact with on a daily basis. Other than that, Rust will be used to build languages like Julia, Python, Go, and a hundred other "better" GC languages in the future. Rust compiled code will always be present, but invisible. Like C/C++, the aim is for Rust to make the world go round. And in that sense, it will allow many more questions of "Is B better than C for Z?" Understanding that will go a long way towards realising why Rust is not "another of a zillion new languages".

It's like asking: "which is better, the skyscraper or the steel that reinforces the skyscraper"? The answer is both and neither. Yes, you can build an all-steel skyscraper like the Eiffel tower, but that will likely always remain a gimmick. Usually, you use steel to enable you to build a cement skyscraper. In the same way, Rust, C and C++ is the steel that goes into making a new programming language 'skyscraper'. Then you can ask whether the R 'skyscraper' is better than the Julia 'skyscraper', but both have a steel core reinforcement which is C in the present case, and the aim is for that to be Rust in the future case.

I'm essentially arguing that a non-GC bare-metal language is completely in a different league from a GC language with larger runtime in the same why steel and concrete is completely different. You can use the one as a replacement of the other, but it should be obvious (at least in the literal case) that they are not truly substitutes. That is why a question comparing Rust and Julia is both valid and invalid. If the conclusion is that Rust is better than Julia at something, that is like concluding concrete is better than steel for paving, it's a little contrived. It should be obvious that they cannot replace each other completely and work best in tandem - like all skyscrapers show us.

10 Likes

Great answer, thanks

By passing parametric type information, it is a great means of moving work from runtime to compile time.
Small matrix operations can be extremely fast. I have a prototype that automatically pads sized arrays to allow for better vectorization, and then use generated functions, CpuId, and SIMD intrinsic to write optimized BLAS microkernels at compile time.
Starting at 5x5 matrices, this is several times faster than StaticArrays.jl, which also makes heavy use of generated functions.
I plan to eventually use masked load/store operations to alleviate the need for padding and make a PR to StaticArrays.

Another application of generated functions I've found powerful is in letting others define their own models. This is coming in lots of numerical work.
You can then use generated functions to specialize library functions heavily on these models -- anything from defining storage to avoid triggering the GC, source to source autodiff, or other any other details related to the class of models they're trying to fit.
One way to think of it is as a high level means of writing your own problem-specific front end compiler.

Generated functions are something I would like to see in Rust.
Take a look at Stan as an example application or Nimble, which is similar and suffers from similar problems).
It is written in C++ (but used from languages with a REPL: R, Python, Julia). Users write a statistical model in a DSL, which then takes several minutes to compile -- not great for interaction!
It makes heavy use of templating, but also has to compile a massive math library every time. In part large, because it uses reverse mode autodiff, and you get a combinatorial explosion of functions based on which arguments you need partial derivatives of.

With generated functions, you can avoid the explosion, by only generating the needed sets. I, define each function once, and input types determine whether to add tracking/storage for that argument.
Or maybe Rust's macros could already be used for either that, or a source (user defined model written in a DSL) to source autodiff that follows reverse mode algorithms, minus all the boiler plate you're relying on getting compiled away.

3 Likes

Right, the natural Rust equivalent to Julia's generated functions would be generated trait implementations, where the body of the implementation is allowed to depend on const generic parameters of the types involved. It'd be an interesting followup to const generics.

5 Likes

I'd like to point out that Evcxr project emerged just recently, which implements REPL and Jupyter kernel and works reasonably well.

4 Likes