I was recently contacted from a person I had never met before, asking for some comments on Nim and Rust. Since I have taken a little time to answer, I figured I might as well publish my thoughts.
Please, note that I do not intend to bash Rust, and in fact I am looking forward to their 1.0 release. Below, I mention some interactions I have had with the members of the Rust community. In each case, people have shown to be kind and helpful, so my perspective on the community is definitely positive.
Also, links are to the 0.12 documentation, because if I recall correctly that was the version I was using when the events took place, but the substance remains the same with the latest releases.
EDIT After publishing this on the Rust forums I received a pull request with an improved version. This version is slightly less verbose, but in order not to break links, I am pointing to a Github revision that precedes these modifications.
I hope you don't mind if I contact you directly, and ignore if you're offended, but I saw your post on the parasail email list and looked at your KMeans benchmark.
In particular, I was interested in your statement that rust was hard and that you found Nim much easier. I'm interested in both and did some additional searching and found this from Dennis Felsing and he too likes Nim a lot.
I was wondering if you could expand on your dislike of Rust?
Again, feel free to ignore this email.
I will be happy to expand. First, I should make clear that I do not "dislike" Rust. It is just that, while I appreciate some ideas in theory, in practice i have found it hard to use.
I would choose Nim for projects where I can afford a GC, and evaluate case by case for other projects.
As you probably know, Rust has a compile-time mechanism that guarantees that you make an appropriate use of pointers, avoiding such errors as: use of uninitialized pointers, double free, use after free and so on. Essentially, it does this through the use of compiler-enforced smart pointers, such as unique pointers or shared pointers. To achieve this, the notion of a type is expanded in such a way that each pointer has a type and also a lifetime, and the compiler checks that the use of lifetimes is correct. I will not expand, as many resources on Rust explain the mechanism in detail.
Now, Nim does not have anything like this. In Nim, you either use the garbage collector, or you manually manage pointers, with the associated possibility of errors. That is, while you can program at a low level in Nim, you do not have the safety net provided by Rust. This is no more or no less safe than managing pointers in C, C++ or D.
I believe that much of the difficulty I found in Rust was due to this mechanism, and another part was probably due to the fact that Nim has a much cleaner syntax, which you can easily check by comparing the two implementation, say, of Point: Nim (first 18 lines) vs Rust
Now, what were in practice the stumbling blocks that I found?
1) The algorithm uses a hash map indexed by points. Now, how do I put a point into a hasmap? My first attempt used
But this lead me to the problem of hashing points. This in turn, would require me to hash each coordinate, but
Double in Rust does not implement the trait
Hash. Now, there are good reasons for doing so, due to the subtleties of floating point arithmetic, but they did not apply in my case.
2) So I had to find an alternative solution. Let us use
Unfortunately, the operation
find on a
TreeMap returns an immutable reference to the value (even though the
TreeMap is mutable!), but need to mutate it. With a little help online I was able to make it work, but the solution proposed was kind of hacky and certainly not very clean (look at the answer!). It also involved some copying.
3) Then I tried to parse JSON to read the points. The obvious attempt did not work and the proposed solution involved a 11 line definition to define how to parse a point.
4) At this point I went back trying to use an actual
HashMap. I wanted to write a
Hash trait for
Point, but this required me to define a method that had a
std::hash::sip::SipState in its signature. I really did not want to bother understanding what that even is, so the thing stayed slow for a while.
5) Then with the help of one of Rust mantainers here I was finally able to get it working as it is now. Notice the use of
mem::transmute that I don't think I would have been able to find myself.
In Nim, I was able to obtain an implementation (garbage-collected) of about the same running time in the very first morning I had ever heard of Nim, essentially by literal translation of the Python version. It turned out I had made an unnecessary copy, and once that was cleared out, the Nim version was faster (!).
So, while I believe that Rust has good principles, I also think that languages should have a decent ergonomics, and Rust fails at that on many fronts:
SipHashabove, or the mutability issue on
TreeMap::find, or the hoops to jump to parse a JSON file)
There are other small things that turned out not to be a problem, but were nevertheless overcomplicated. Look at this line
Why do I have to convert the vector to iterator and back, instead of being able to map it directly? I guess this is to leave the possibility to map into another container type (say, from a
Vector to a
List), but adding a map function on Vector would have not prevented this more sophisticated use.
There are also a lot of things that Nim gives you and Rust does not have, such as
and probably more
What are the pain points of Nim? There are not many, but I will mention:
Now, don't take my words: there may be many inaccuracies above, as I am a beginner both with Rust and Nim. If anything, they prove my point better: even after going through a lot of hoops, I still do not have clear many things about Rust.
I hope this helps you!