First impressions with Rust

Hi, I am new to both Rust and this forum, having made some experiment with Rust recently.

I was recently contacted from a person I had never met before, asking for some comments on Nim and Rust. I took the time to answer him with my experience, and he suggested that it might make sense to share my views with the Rust community.

You can find my experience here. Please, note that this is not meant to bash Rust in any way, and in fact I will continue to follow it development with interest, especially now that it's landing 1.0. Also, I have probably written many inaccuracies. The post is meant to reflect the experience of a Rust beginner. Still, I would be happy to stand corrected, and learn something more. :slight_smile:

2 Likes

TL;DR Your article has large positive bias towards Nim. Hard to take rest of it at face value.

Rust is verbose in some places. But I'm not sure your point about 10 vs 1 lines stands under reasonable scrutiny. If Nim is a white-space sensitive language, then that's explains most of "less lines" required part. White-space sensitive language have no need for { or } so you can shave couple of lines here and there.

First your Point would make more sense as named struct:

  pub struct Point {
      pub x: f64, 
      pub y: f64,
  }

Which makes your add implementation quite shorter:

 impl Add for Point {
      type Output = Point;

      fn add(self, other: Point) -> Point {  
         Point { x = self.x + other.x; y = self.y + other.y }
      }
}

You could probably trim out some white space to achieve less lines, but its probably against style guidelines.

Personally I'd love to be able to say

  fn add (self, other: Point) ->  Point(self.x +other.x, self.y + other.y)

But I think at the time of 1.0.0, the focus is mostly on getting something done, not necessarily making it as pretty as possible. I think region syntax and error reporting is more crucial than having X amount of lines, or not. If project Coin in Java teaches anyone anything, it's that it is possible to pretty up the syntax at later point.


Also I think overall tone is basically Nim good, Rust bad - you don't list any benefit to Rust, only example and bad sides. I'm pretty sure any language has good and bad sides.

2 Likes

Yes, I know that my article has large positive bias towards Nim, and yes, I do not list Rust benefits. This was not meant to be a blog post, but rather an answer to someone asking why my preference was leaning towards Nim instead of Rust.

I am not here to complain. The OP asked me to share my experience with the Rust community, and I just copied it. I am sorry if it comes a little harsh, but I tried to describe objectively my experience

3 Likes

Don't take it too harshly, I do understand a part of your argument. That Rust is too verbose in some aspects, but I believe that's mostly due to C/C++ -ish syntax and focus on making really big stuff work Ok. I mean you could spend your time tarting up struct syntax, or you could solve some bugs with Borrow Checker/Associated Traits.

Out of your reasons:

  1. Verbosity is partly due to language being curly braced and highly subjective*
  • I'm confident SipHash is like that due to Reasons™ (I assume it's some weird crypto thing)
  • cargo point was lost on me. You mention an example but never elaborate which one? Your own? It looks not much different than a standard Python/Java directory structure.
  • documentation is poor. This one is legitimate, but dependent on stabilizing API. SipHash API is unstable.

Your arguments aren't strong and don't coincide with my experiences.

* Personally I hate white space based syntax with a burning hatred of a thousand Rigel's.

To add to the point about "one line vs. 10", the code can be written as:

    fn add(self, other: Point) -> Point {
        Point(self.0 + other.0, self.1 + other.1)
    }

Even without changing the Rust struct to have named fields like Nim.

About 3, yes, I am refering to the kmeans example. For some reason, cargo wanted me to put every module inside its own directory and name the file - say - point/mod.rs, instead of a more liberal point.rs. Not quite a showstopper, but it took me some time to figure out the correct structure for cargo projects, including lib.rs and stuff. And still, I am not quite sure how to make a project that is made of multiple crates

I see. I think this feature was not yet available in the language when I wrote kmeans

I also should add that I like Rust under many aspects - in theory. But in practice, that has not translated to a nice programming experience. I also used to like some ideas that have later been ditched, such as typestate, per-task GC and m:n threading. In a way, the initial idea of Rust was quite appealing to me (I have a background more oriented towards high-level functional programming), but then it moved too much in the C++ direction.

2 Likes

I doubt this. It isn't a well advertised feature but it was in Rust least since August 2014.

1 Like

Sorry, I liked your post by accident. Stupid touch controls.

All the stuff you mentioned carry significant problems:

  • per-task GC, had bad GC and added overhead, while clashing with goals of a system language
  • m:n threading was a source of bugs and performance issues
  • typestate was overly complex IIRC, someone correct me on this.

I am curious: why per-task GC would have prevented the cargo package manager? The two things seem pretty unrelated.

It's a bit contrived example so I'll remove it, but if Rust had GC, integrating it with Ruby would be next to impossible - because two GC would interfere with each other. Without it, Yehuda Katz wouldn't be interested in Rust, without that the current cargo package manager would look quite a bit different or not exist at all.

I just tested this. You are wrong on this account as well.

Here is a test. Run cargo new test. Enter test folder. Add point.rs file. This should be the tree structure:

   - Cargo.toml 
   + src
     | 
     +-lib.rs
     |
     +-point.rs

     //lib.rs:
     pub mod point;

     //point.rs
     //Empty

Works with cargo build. Now if you declared a mod point; in point.rs that would be an error, telling you to move the file to its own module, because the file itself serves as a mod. When you declare mod point{} in a file point.rs, you aren't creating module point, you are creating module point/point, which is undeclared in your lib.rs!

Clarification:You can declare a module point either by making a folder called point with mod.rs manifest or you can create a file point.rs. Usually the first is the recommended scenario, because it allows for more expansion. Note: module system allows for more gymnastics than just this! You can have several mods in a single file or you can have mods split along several files. It's complex, but for this purpose only single file/single folder is enough.

This seems like a case of both DRTM (Didn't read the manual) and needs better documentation.

If anything, this proves my point even better.

First, there are two ways of doing the same thing, namely declaring things in a namespace: both the file name and the module declaration inside it count. So, if the file is named point.rs, there is no need for the module declaration, but otherwise the module declaration is needed. This is unlike - say - Scala (file name is ignored) or Python (no need to state the module name). I still mantain that this is confusing, and unnecessarily so.

Second, I actually read the cargo documentation, but it is quite sparse and does not cover this topic at all. I now see that there is something here, but I did not find that when I was trying to do this.

1 Like

I am not advocating for Rust to be garbage collected - that would make it far less interesting than it is. I am only saying that I liked having optional, task-scoped GC

This looks like some good remarks. Personally, I think Rust is great because it addresses a lot of hard and/or inconvenient issues with other languages. A lot of the pain points I have with Rust are consequences of that. A great example is how hard it is to work with floats at times: it's hard because nearly every other language is doing it wrong!

Which is also a great demonstration on why they keep doing it wrong: doing it right is a pain in the ass. :smiley:

Reading through your notes, I keep thinking "oh, there's a good reason for that... but it doesn't help if you don't know what it is". I suppose that sort of thing is primarily Steve's problem. :stuck_out_tongue:

Just to track my own reactions to your notes:

  • find didn't work because it's defined to only work on immutable objects; the TreeMap instance being mutable is irrelevant. Just below it in the docs, though, is find_mut, which was what you wanted. That you didn't (see it/know to look for it/recognise the distinction) suggests that the docs at that point weren't doing a good job of noting how important that distinction is in Rust.

  • I vaguely remember trying to do something with Hash around the same time. It was rather obtuse. The current API is... well, I just looked it up, and I'm frankly baffled at a first glance on how to use it. [1] The good news is that part of Steve's contract is for him to write examples for every type and function, or at least as much as is possible. Once that's done, Rust should have pretty excellent coverage when it comes to practical examples. That's obviously little comfort back then.

  • I think the JSON thing is a bit unfortunate. It comes down to either: using a Vec<f64> instead of a fixed-size type, waiting for Rust to get generic value parameters, implementing the very general-purpose Decodable interface, or manually unpacking the JSON AST.

  • I find it amusing that Shepmaster said "cast" when he was talking about transmute. In probably any other language, bitcasting would be a language feature. Rust is all like "pfft; stick that crap in a function". Your point is pretty fair: how on earth is anyone looking for bit casting going to know to search for "transmute"?

  • I'd say that the language is verbose in "compared to Python or Nim" terms, but I don't see it as being unduly so. Especially when you consider that there's been a concerted push to remove unnecessary syntax sugar ahead of 1.0 stabilisation. That said, if let got added because it was such a widely useful bit of sugar. Also, there's lifetime elision, which is pretty damned massive.

    The example of Add is pretty bad for Rust in a straight-up comparison, but I think it's also a little misleading. I mean, Rust doesn't have function overloading, you're not using tuple member access or using a struct with named fields, you're counting optional whitespace, and trait syntax has to deal with multiple dispatch and inference issues that an overloaded function doesn't.

    Again, completely fair to point out that it's easier to define addition of a type in Nim.

  • The standard library being complicated is... probably even truer today. Rust is very abstraction-happy, and that's a good thing. It's also a bad thing. I don't know about overcomplicated, though. That implies to me that the complexity serves no purpose, which I don't think is true. I mean, the Entry stuff is a little obtuse at first, but it's a very impressively clean solution for the problem at hand.

  • I deeply suspect your problems with Cargo were documentation-related. Cargo, at the start, had very sparse documentation, and some stuff was just not at all obvious. You should have been able to do the whole thing in a single directory. I haven't really read through the Cargo guide recently, but I think things have improved on this front.

    I'm kind of surprised you didn't complain about the module system itself. That always seems to throw people for a loop! :smiley:

    Although now that I think about it, it seems like at least part of your problem with Cargo was, in fact, a problem with the module system. I wrote up an article about it a while ago, and Steve wrote a pretty detailed one for the guide book... but again, it's an issue of timing in your case, and making sure people can find the information in general.

  • Again, yeah, Hash is a pretty nasty thing to have to deal with.

  • The "overcomplicated" line is, I think, rather unfair. The reason Vec doesn't have map is because it's defined on Iterator. It makes little sense to pepper the code with special-cased shortcuts to general functionality. That starts to get into questions of "which iterator composition methods should be defined on containers?" and "in cases where a container has multiple types of iterators, which do we use?" and "should the shortcut return an iterator or the container type itself?". Shortcuts would be nice, but I also think in this case, they'd be a mistake.

    One thing that might help, however, are HKT (Higher Kinded Types). These are planned for eventually (hopefully in the near-term), and would allow for things like Vec to express the fact that they can be turned into iterators. At that point, you could write an adapter that takes an Iterable and exposes the composition methods itself, without the scaling problems of doing it at the moment.

    Also, it's not just so you can do things like Vec to List. Iterator is basically the fundamental compositional unit of sequential processing in Rust. There are whole libraries that are just about defining new Iterator-based compositions. It's not just about Vec; literally every type that can be turned into an Iterator gets map.

    Now, having said all that, allow me to blow a hole in my own argument: Option::map, Result::map, and Vec::map_in_place (though that one does have additional restrictions). :smiley: [2]

  • On your points comparing Nim to Rust directly, Rust does have control over the calling convention. Rust's interop with C is pretty great, with the exception of union which Rust just completely fails at right now (you have to resort to transmute).

  • I can't comment on Nim macros, as I'm not familiar with them. I can guess that Nim templates are less verbose than Rust generics, though. To be fair, after years of working with "conventional" template systems (C++ and D, mostly), I much prefer the way Rust does it, as it allows generics to be type-checked at their definition, instead of at their expansion. They're so, so much easier to debug problems with!

  • Also, Rust does support dynamic binding, although I'm not sure what exactly your referring to here in regards to Nim. Short version: as long as a trait is object-safe, it can be used for static dispatch and dynamic dispatch.

I'm always happy to see stuff like this, because it's hard to keep track of how the language appears to newcomers when you're already waist-deep in it. Thanks for putting it up.


  1. Ok, so there's a Hasher trait now, but the only operations defined are reset and finish. If I'm implementing Hash on a type, how the heck do I feed it anything? :confused:

  2. I had links to the documentation, but the forum apparently thinks I'm not trustworthy enough. Fine, be that way. Stupid forum...

2 Likes

Since I made a vague comment about the module system in my larger reply, and having just read this, I thought I'd just chip in:

It looks like you're still a bit confused. To be honest, @Ygg01's explanation confused me, and I wrote a whole article on this stuff. :stuck_out_tongue:

Taking your Rust kmeans as an example, everything inside the kmeans library (i.e. everything outside bin/kmeans.rs), could have been done in a single file like so:

extern crate serialize;

pub mod algo {
    // Contents of algo/mod.rs
}
pub mod point {
    // Contents of point/mod.rs
}

All something like pub mod algo; (i.e. a semicolon instead of braces) does is tell the compiler to read the contents of the module from either algo.rs or mod/algo.rs.

This is unlike... every conventional language I can think of, in that the module hierarchy usually isn't part of the source itself, but rather how the source is organised on disk. I actually quite like that Rust puts this back in the source, as it means the literal source text is a more complete description of the program. It also means you can do things like define modules inline. This is really nice when you're talking about a very small module, or a module that's really just an implementation detail.

But, as you correctly noted, this does cause confusion.

Your point was that the convention for modules was unable to express in a way you wanted. I just demonstrated it was possible.

Had your point (in the blog) been on the complexity of the module system I'd agree somewhat. Having several modules in same file and same module in several files is quite daunting task to understand. It wasn't about that.

As for only one way to do it, IIRC Python also allows similar stuff with either having your module be located in a single file foo.py or having a directory foo with __init__.py. The reason folder + mod.rs is used so it will potentially allow for finer grained control of visibility. You might want some things you use internally not leak out to library users.

Hmm, which part confused you? I hoped it would be clear that you can declare module foo by either making a file foo.rs or by making a folder foo with a file mod.rs (or declaring a mod foo { ...}). Rust has three ways to define modules.

Thanks for the new user report! I ran into most of the same stuff when I started learning rust in earnest a few months ago. I think it is clear that Rust will never be an elegant language. It will probably always be a hairy language used only by people who need to do things that are not possible in other languages. There will probably never be a cute manual like leanyouahaskell that can cover darn near every concept in the language; people will probably learn mainly by looking at pre-existing examples for every single feature they need to use. For example, it also took me a long time to figure out how to do trivial stuff with Cargo because there was little documentation, and Rust's module system is complex enough that you will NOT figure it out just by making random but educated guesses on how it works. But within a few months, "If you are a beginner always start with a sufficiently complext template project" will probably just become standard practice.

1 Like