Awkward guessing of types in Rust

You use some std or crate function that you know returns a simple number. However, without poring over the documentation or the source, there seems to be no way of telling exactly of which type. Is it a u64? Is it a usize? Is it an isize? Is it an i64? I have not even started on other possible sizes in bits. Yet, the moment you try to use it, say in a comparison, the compiler insists that you know its exact type. Automatic inheriting of the type in let declarations does not really help, on the contrary, it just hides the type from you further. This is awkward and not exactly convenient. Of course, it applies to all types, often obscure wrapper types returned by all kinds of utilities.

1 Like

I tend to be happy with sublime text and the rust extended package, which on save gives an inline error about the type that the function returns -- the compiler error messages are aesthetic enough to indicate what I've incorrectly assumed.

One method to have the compiler tell you the type is to assign it to a variable with the wrong type:

let foo: () = make_an_integer_type();

this will fail with:

error[E0308]: mismatched types
 --> src/main.rs:6:19
  |
6 |     let foo: () = make_an_integer_type();
  |                   ^^^^^^^^^^^^^^^^^^^^^^ expected (), found i16
  |
  = note: expected type `()`
             found type `i16`

Notice how the error message tells you the type is i16. Note that if you know you need a specific type, you can use from to convert it if the conversion is guaranteed to be lossless:

let foo = isize::from(make_an_integer_type());

This will fail to compile if the conversion is not lossless, and of course, if you use a type without lossless conversion, e.g. in this case an usize, it will tell you the actual type:

error[E0277]: the trait bound `usize: std::convert::From<i16>` is not satisfied
 --> src/main.rs:6:15
  |
6 |     let foo = usize::from(make_an_integer_type());
  |               ^^^^^^^^^^^ the trait `std::convert::From<i16>` is not implemented for `usize`
  |
  = help: the following implementations were found:
            <usize as std::convert::From<bool>>
            <usize as std::convert::From<std::num::NonZeroUsize>>
            <usize as std::convert::From<u16>>
            <usize as std::convert::From<u8>>
  = note: required by `std::convert::From::from`

Notice how it says std::convert::From<i16> is not implemented for usize, making clear that it cannot convert from i16 to usize losslessly, as negative integers are not valid usizes.

5 Likes

Thanks, that deliberate error is a nice hack that I did not think of!

I don't know which editor you are using, but you should investigate if you can use rust-analyzer with that editor, as it will allow you to immediately view types and documentation of functions if you hover the function call.

1 Like

I use fairly primitive Geany, which has so far been adequate to my needs (syntax highlighting, cargo commands tied to function keys).

I'm curious, how else would you expect to find out the types if not – ideally – from the declaration of the function and/or its documentation?

I'm also curious how/why this problem is specific to Rust. IMO it applies equally to all statically-typed languages. In fact, I'd say it applies to all languages. In a dynamically-typed language, there isn't even a function declaration to guide you, you have to rely on the documentation (or, worse, look at the implementation of the function and its transitive callees).


To me this is a non-issue, by the way. I know the signature of the most frequently-used (either stdlib or 3rd-party) APIs by heart, and if I encounter something new, I have to look up its documentation anyway in order to be able to use it at all.

4 Likes

That is all very true.

However you would not believe the number of times that I, as a newbie to Rust, ran into exactly the same problem. I could not for the life of me figure out the right type to make the compiler happy. Running around the documentation did not help. Perhaps not with simple integers and such but when things are wrapped up in Options of Arcs of Vecs of Trait this that and the other contortion.

Then I hit on the idea of deliberately specifying a type where it is being inferred, as alice describes above. Boom the compiler immediately offers up the required information for me to proceed. I used String rather than () but same idea.

It did cross my mind that I was being a bit silly doing this. Until I was watching a Youtube video of someone live coding something in Rust. It made me smile to find that they ran into the same problem and came up with the same solution!

I would not say the documentation is lacking, but as a newbie it's not always so transparent, it requires one knows the language quite well to be able to read the API docs fluently.

It's rather like the problem with compiler error messages in general. When you are starting out they can be totally incomprehensible. Over time one learns ones way around the language better and becomes familiar with a lot of common error messages, eventually instinctively knowing what to do about them without even reading them properly.

3 Likes

It seems that Rust-Analyzer is the way to go.
Presumably somebody has already realized that when the compiler knows the type perfectly well, it is somewhat unreasonable to leave you in the dark and then complain: "haha, you guessed wrong".

The documentation will show the entire signature for a function including its return type, although you have a point in that it takes a bit of practice with the language before you are accustomed to mentally parsing the information it provides.

For example say I'm wanting to know where the currently executing binary is located on disk, I would go to the docs for std::env::current_exe() then look at the function signature at the very top.

The bit after the -> says Result<PathBuf>, meaning the operation is fallible and will return a std::io::Result (hover over the word Result), with the "happy" variant being a PathBuf (owned buffer containing a file path).

Things can get a bit more difficult to read when complex generics or lifetimes are involved, but as you become more familiar with the language things will get easier. And of course, if you get stuck on something you can always ask here or on discord and someone will be able to point you in the right direction :slightly_smiling_face:

1 Like

What you describe is simple enough. Once one knows what a "Result" is in the case of your example.

I don't have an example to hand, lost in the depths of time, but for sure I have found my self with a "thing" that returns a "thing" and finding out exactly what either of those things are was not trivial.

I'm pig headed enough to waste considerable time trying to figure something out for myself, from the docs and/or by experiment and chatting to the compiler, before I give up and ask for help.

Aside: Years ago I worked with an Ada programmer who joined our team, we were working in PL/M. He was a clever chap and said "Don't worry about language training, I just throw Ada at the PL/M compiler then tweak it till it stops complaining".

For sure I have ended up here when I get really stuck and no doubt will again :slight_smile:

2 Likes

To be honest, this sounds a lot like you somehow expect all documentation to spoon-feed the reader every necessary bit of information which is transitively necessarily to have in order to understand the semantics of some piece of code, with approximately zero knowledge of the language or the ecosystem. I think that's neither feasible, possible, nor desirable. Just try to imagine what it would be like if the documentation of every function kept giving you a 2-page mini-lesson about what a function in programming is, what's a formal and actual parameter, what it means to return, etc. It would be a disaster.

(By the way, rustdoc already provides links to the documentation of all the types found in the signature of a function, likewise it also links to traits a type implements, as well as functions and associated items of every trait.)

One has to have some background knowledge and experience in the language in order to be able to make sense of code written in that language. This fact cannot, and need not, be "fixed".

1 Like

It seems like you're making some kind of slippery-slope argument here. I'm sure you don't intend it as such, but it comes across feeling in bad faith. I think rust has great documentation, but knowing which group of magical symbols to invoke to get to the doc page you need is, with some frequency, frustrating.

Without the aid of the intentional-type-misinterpretation trick above, type misunderstandings can get exacerbated pretty badly through a combination of generics and auto-ref-and-deref. I've gotten absolute soup out of what I though were simple operations, and untangling them took a lot of digging through layers of docs to find out how I ended up where I did. This is often exacerbated by the layered packages and reexports that are common in the rust ecosystem.

My point is, I don't think there's any problem for calling for a bit more documentation, especially some careful explanation of what is returned from a function. Often, the only mistake I see is assuming the user can trace back the three layers of generic types hidden behind a newtype. Nobody is actually calling for each page of function documentation to explain what a function is, or how it's better than memorizing labels for gotos, or why we'd use a Result instead of some sentinel value. It feels really disingenuous to insinuate that.

4 Likes

I mean, I imagine most of these issues would be solved by adding some examples to the documentation of things.

4 Likes

We use Result instead of a sentinel value because sentinel values are a multiple-decades-old anti-pattern in programming, which has lead to numerous bugs, and that Rust solves elegantly using the sum type that is Result. This is exactly the sort of background information that I'm talking about; it's something that you discover by reading the Book or any decent tutorial on Rust, and get a good feeling for it after a couple of weeks of having used the language. Idioms are idioms because they do not warrant repeating the underlying explanation whenever they appear. There's nothing disingenuous in questioning the opposite.

That's explicitly not what OP asked for, though. What was written is:

To me, this is clearly not a request for more documentation, but a complaint against how you have to know the language well in order to be able to use the documentation. This is what I dismiss as a non-issue, because I find this fact to be natural and inevitable.

3 Likes

Yes. Examples are very useful even for experienced users, because they provide a quick way of getting started. I'm a strong advocate for copy-pasteable examples, as using them is a lot more convenient even when you already know exactly what you would write by yourself.

On the contrary. I did say above "I would not say the documentation is lacking, but as a newbie it's not always so transparent, it requires one knows the language quite well to be able to read the API docs fluently."

I think the Rust documentation is excellent. Whilst at the same time having sympathy for newbies that have difficulty with it. Being one myself.

As a newbie there is a lot of unfamiliar stuff in Rust, enums are not enums as we know them, what's all this about traits, why doesn't anyone use loops as we know an love, that's before we get into all that mutability and borrowing stuff.

The issue then, for the newbie, is all that unfamiliar lingo is used in the documentation. Which is to be expected of course. You see the chicken and egg problem here?

On the other hand, it did take me two days research and experimentation, an issue and discussion on github and a discussion here before I got postgres.rs working with TLS.

I'm about to go round that loop again with using thread pools with postgres.rs. I can't for the life of me fathom it, the examples seem to be out of date and just don't build.

Maybe I'm just slow...

4 Likes

Apologies for the wrong assumption, then. However, in this case, I'm afraid I don't understand what you are requesting in general. How should we fix this chicken and egg problem? As I mentioned, I don't think it's possible.

On the other hand, in the specific case of postgres, the docs and examples being out of date is definitely an issue. Since doc-tests and examples in Rust are compiled and run, I'd even say this downright constitutes a bug. That's something that individual crate authors can, and should fix.

1 Like

I'm not actually requesting anything. Just sympathizing with rustafarian's post and noting that I have observed others having similar difficulty. And arriving at the solution alice outlined above.

In my case I suspect, reading "the book" from the top again would help. Or perhaps someone has written a Rust for C programmers book :slight_smile:

I glad you said that. Gives me hope that I have not lost my mind :slight_smile:

2 Likes

Frankly, if it was not for the few examples that I could actually find in the docs, and help of Alice and others here, I do not think that I could have ever written a significant rust program that actually worked and did something useful.

Perhaps I am wrong but I never considered myself "slow" in programming before, having mastered many other languages without any such difficulties.

The whole experience of getting lots of compilation errors, often struggling to understand why, was like learning to program for the very first time again.

I have been teaching programming and I know that beginners often struggle with the feeling: "why oh why does 'the computer' have to make life so difficult"? That is exactly the sort of feeling that Rust specializes in cultivating :wink:

Yes, I know that for example in C you can get "illegal memory reference" run-time errors but that on the whole has been a lot easier to deal with than all these baroque strict type constructs.