Difficulty writing code (code completion, docs issue)

Hello,

I'm seeking advice regarding the actual code writing in rust. Please assume an experienced programmer who learns rust, coming from the C++/Java worlds.
While understanding rust's philosophy and core concepts does not seem like an issue, the struggle comes with practical scenarios like the one described below.
Please bear with me I sound silly, but perhaps we're doing it wrong? Perhaps these are bugs/issues to be solved and I should report them?
That said - issues like these hamper our productivity and irritate newcomers and so are a concern.
I'm looking for any kind of advice.

OK, so imagine I need to generate a random integer between x and y. I could google for that, but it just so happens I can ask colleague Joe about it, and he says:
"Hey, we're using this standard rand crate. Take thread rng or something from it and you're good"

That advice sounds plausible and so I type (vs code with rust-analyzer):

let mut rng = rand::

and the IDE obliges with random() and thread_rng() completions. (NOTE: I'm not allowed to include more than 1 media item in a post, so no screenshot here...)

OK, great! There's this thread_rng that Joe was talking about. So I end up with:

let mut rng = rand::thread_rng();

Now, let's see what can I do with my rng:
(again, no screenshot due to the media item limit, but the available completions to let x = rng. are:

clone()
clone_from()
clone_into()
into()
to_owned()
try_into()

Ooops... this doesn't look promising. There doesn't seem to be anything in the popup that looks related to random number generation.
Joe is not here, so I take a look at the docs:

All right... there are 4 interesting functions there:

fn next_u32(&mut self) -> u32
fn next_u64(&mut self) -> u64
fn fill_bytes(&mut self, dest: &mut [u8])
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error>

Joe is back, so I bug him again:
"Hey Joe, this ThreadRng looks pretty low-level. Ain't there some API to generate integers in range? I don't feel like using next_u32() % something"
"What do you mean? There are these gen* methods in ThreadRng"

Oh, are there?
Let me try... I type:

Which yields more results... Interesting...
And I'm totally baffled when I type rng.gen which shows 4 completions (sorry, can't attach another screenshot):

gen()
gen_bool()
gen_range()
gen_ratio()

Eventually, I'm able to locate the get_range function and use it, but this is not a great experience and relying on somebody else's expertise for trivial things won't scale.

What is going on?

Is it a rust-analyzer bug that some methods are hidden from completion (it's not like there are gazillions of them...)? Is it perhaps a configuration issue? Any way I could help to debug or improve it?

Why wasn't I able to find these gen* methods in the docs?

There may be perfectly good answers to these questions, but the current state seems to move us through more hardship than needed or expected during the transition towards rust :slight_smile:
Please help :wink:

FWIW, VS Code version:
Version: 1.73.1
Commit: 6261075646f055b99068d3688932416f2346dd3b
Date: 2022-11-09T03:54:53.913Z
Electron: 19.0.17
Chromium: 102.0.5005.167
Node.js: 16.14.2
V8: 10.2.154.15-electron.0
OS: Linux x64 5.15.0-52-generic snap
Sandboxed: No

Tested rust-analyzer v0.3.1285 and v0.4.1284

The gen methods are parts of the Rng trait, this is documented in the Quick Start section. It has to be in scope for rust-analyzer to show completions.

I do think the docs here are not the most explicit for beginners (for example, instead of use rand::prelude::* a more explicit use clause could be shown). But beginners should learn about traits at some point or another.

2 Likes

I have to read the documentation for a crate first, before trying to use it.

1 Like

Thanks for the answer. It's brief but helps to put the puzzles in the right place.

We know about traits. We know about extension traits.
But we don't know what extension from the dependencies we could use. We don't learn all the docs by heart, we don't draw diagrams with extensions. We are too lazy for that.

Do I correctly interpret your "It has to be in scope for rust-analyzer to show completions." to be more like:
"declare what you need, then rust-analyzer will help you complete using that knowledge"?

I'm thinking the opposite is more practical:
"rust-analyzer - please tell me what can I use from the crates I already depend on, and if I decide to use something that's not used yet, just add the import...".
And it kind of works that way - e.g. after one types g it suggests gen() even though there's no use rand::Rng; anywhere. The use is only added after writing gen(). But it seems to be half-pregnant in that regard - e.g. it doesn't suggest gen_range() after g or any of get* functions after .

Regarding the docs: if you read the aforementioned Quickstart or RngCore docs it's more or less clear what you can do, but reading all the docs of all the crates to find what's out there is not practical. (Yes, I understand that a struct like ThreadRnd cannot say about its extensions, especially in general, and yes, I think Quickstart is a very good place to mention them)

That said - the leap from rng. to Rng docs is IMO too big to make that connection efficiently without a priori knowledge and these kinds of issues are a drag for us.

This sounds like a problem with IDE to me, but maybe it's just me and my bubble.

Yeah, we only show completions for trait ("extension") methods if there's already some kind of match:

That's a tough thing to solve, because often times you don't want extension methods from random traits polluting the completion list.

I would say a good library design should not require users to import traits. Traits are a great extension point mechanism, but they are not great as an actual "API". It's not only for IDEs: as a human, if a method call goes through trait dispatch, its very hard to understand what actual code is being called.

6 Likes

I have to read the documentation for a crate first, before trying to use it.

That's the point. Usually one doesn't need to read the documentation to do a simple thing in many other contexts.
I'd read the documentation to learn if rand is using I don't know - maybe Mersenne twister, or elliptical curves, or is it a secure RNG, or whatever. I may be wrong, but my intuition is that a simple (API-wise) crate should be discoverable top-to-bottom at the moment I use it. It's orders of magnitude faster than reading the docs.

That said - I don't want to get into this particular example too deeply, because it clouds the bigger picture - which is extension discoverability.

IntelliJ IDEA has the opposite problem: it shows you the whole world and automatically imports traits if needed, which is great for discovery… except it also gives you all the completions from transitive dependencies (eg. Itertools if one of your deps depends on itertools) which may not be what you want, especially if the dep is optional! OTOH, often it is what you want – if itertools is already there, why not use it? – but there’s also a lot of noise in the form of extension methods added by random crates not relevant to what you’re doing, so at least there should be some indication of which crate/trait the method is from.

One of the ideas for Rust that has come up a few times is “inherent traits” where a specific trait impl for a specific type (here, impl Rng for ThreadRng {...} is marked to be treated as if it were an inherent impl (impl ThreadRng {...}) meaning that its methods are always available even if you didn't import the trait. I think that would be a good idea and address the discoverability problem in this thread — but it doesn't currently exist, and without something like it, as already noted, there's a dilemma where showing no trait methods (or rather, only those already in scope) is not helpful and showing all trait methods is too verbose (depending on which dependencies your project has), because there are no cues that say "this trait is the means by which you use this type".

4 Likes