Documentation for type inference rules?

This started as discussion in the URLO, but I after poking around I have realized that I couldn't find the documentation which explains how type inference rules work with enough details to reason for cases like these. Even in the documentation for the rustc developers.

TL;DR: Somehow if you give compiler an option to read usize — it takes it, but if you say “hey, you can read both usize and T” then there are no ambiguity: compiler just decides you would now read T exclusively. And I couldn't even understand if that's bug or feature (is that actually described somewhere?).

Note: somehow everything works with vector! There are no ambiguity and both vectors or usizes and vector of T can be read!

Is there any way which may help one to understand what is suppose to work and what would fail?

Somehow recommendation “just add or remove restrictions randomly till compiler would be happy” doesn't sound reasonable.

The code in question looks like this:

impl<T> Matrix<T> {
    fn read(path: &str) -> io::Result<Self>
        where BufReader<File>:
             ReadNumeric<usize> + ReadVector<usize> + ReadVector<T>
    {
         …
         let nnz = f.read_numeric()?;
         …
    }
}

And as you can see nnz (of type usize) can be happily read. But if you do the following:

impl<T> Matrix<T> {
    fn read(path: &str) -> io::Result<Self>
        where BufReader<File>: ReadNumeric<usize> + ReadNumeric<T> + 
             ReadVector<usize> + ReadVector<T>
    {
         …
         let nnz = f.read_numeric()?;
         …
    }
}

Then, suddenly, nnz is inferred as T:

   |
52 | impl<T> Matrix<T> {
   |      - this type parameter
...
61 |             data: f.read_vector(nnz)?,
   |                                 ^^^ expected `usize`, found type parameter `T`
   |
   = note:        expected type `usize`
           found type parameter `T`

And even if you try to specify which type you want T is still preferred:

impl<T> Matrix<T> {
    fn read(path: &str) -> io::Result<Self>
        where BufReader<File>: ReadNumeric<usize> + ReadNumeric<T> + 
             ReadVector<usize> + ReadVector<T>
    {
         …
        let nnz: io::Result<usize> = f.read_numeric();
        let nnz = nnz?;
         …
    }
}

leads to

52 | impl<T> Matrix<T> {
   |      - this type parameter
...
61 |         let nnz: io::Result<usize> = f.read_numeric();
   |                  -----------------   ^^^^^^^^^^^^^^^^ expected `usize`, found type parameter `T`
   |                  |
   |                  expected due to this
   |
   = note: expected enum `Result<usize, _>`
              found enum `Result<T, _>`

P.S. Note that code which was supposed to work looked like this:

impl<T> Matrix<T> {
    fn read(path: &str) -> io::Result<Self>
        where BufReader<File>: ReadNumeric<usize> + ReadNumeric<T>
    {
         …
        let nnz = f.read_numeric()?;
         …
    }
}

Type inference doesn't work here at all, but if you resolve all ambiguities explicitly — it works.

4 Likes

Type inference rules are not specified. The only hard requirement is that inference doesn't break: If it works in version X, it must work in version X+1 as well.

When inference doesn't work, how do we find out if it is a bug, or if it is expected or even desired? If there is exactly one possible type that can be inferred for any combination of type parameters, then usually that type should be inferred automatically — unless finding it would be expensive and slow down compile times.

It seems that Rust doesn't consider the return type of a function call to figure out what trait it belongs to: Playground

However, when there are multiple trait bounds for the same trait, Rust can figure out the correct one: Playground

So your example seems to be a bug.

3 Likes

I don't have a source now, but IIRC, inference breakage is allowed, though it may require a crater run to estimate the amount of breakage.

The general rule is that there must be a way to write it in the old version that also works in the new version. But that might require UFCS, for example, in the common case of .as_ref() breakage and such.

1 Like