2

In the reference at Literal expressions, I see

If the token has no suffix, the expression's type is determined by type inference:

  • ...
  • If the program context under-constrains the type, it defaults to the signed 32-bit integer i32.
  • ...

But I get an error from the following code:

let i = (-100).abs();
error[E0689]: can't call method `abs` on ambiguous numeric type `{integer}`

Why does method-call on ambiguous numeric cause error? Is not the type of '-100' inferred as i32?

E_net4
  • 27,810
  • 13
  • 101
  • 139
Hiroto Kagotani
  • 370
  • 1
  • 2
  • 7
  • 1
    This is [tracked on the Rust issue tracker](https://github.com/rust-lang/rust/issues/24124), though it doesn't look like anyone with the required knowledge has ever looked into this. I believe this is a type inference bug, but since it's trivial to work around with `(-100_i32).abs()`, it's not a priority to spend resources on fixing this. – Sven Marnach Mar 31 '23 at 08:53

1 Answers1

4

Let us first quote all of the relevant part on that page (emphasis mine):

  • If an integer type can be uniquely determined from the surrounding program context, the expression has that type.
  • If the program context under-constrains the type, it defaults to the signed 32-bit integer i32.
  • If the program context over-constrains the type, it is considered a static type error.

Now, this description may be misleading due to how the verb "constrain" was employed. One is usually familiar with generic types constrained by traits, but the reference is possibly not to be interpreted in that way. Types being under-constrained and over-constrained refer to cases in which additional requirements were imposed over the type which must be considered by type inference, which happens when calling a method of that type.

So no, it does not apply to any kind of constraint over a type. Here is a counter-example.

fn is_signed(x: impl num_traits::Signed) {
    let _ = x.abs(); // can call abs thanks to Signed trait
}

fn main() {
    is_signed(-5);
}

This code (Playground) may seem to "constrain" -5 to require the number to be signed, and since we have implementations of Signed for multiple integer types, this should be ambiguous. But it does, in fact, compile as of Rust 1.63.0.

One other peculiar example.

trait Foo {}

impl Foo for u8 {}
impl Foo for u16 {}

fn foo(_: impl Foo) {}

fn main() {
    foo(5);
}

This code (Playground) does not compile, but not for the reason that you might expect:

error[E0277]: the trait bound `i32: Foo` is not satisfied
 --> src/main.rs:9:5
  |
9 |     foo(5);
  |     ^^^ the trait `Foo` is not implemented for `i32`
  |
  = help: the following other types implement trait `Foo`:
            u16
            u8
note: required by a bound in `foo`
 --> src/main.rs:6:16
  |
6 | fn foo(_: impl Foo) {}
  |                ^^^ required by this bound in `foo`

The integer's type inference mechanism allegedly went for the default type i32, even though the only possible implementations are for u8 and u16. It would be another ambiguous situation regardless.

However, things are clearly different when one attempts to call a method directly associated to a type, as done in (-100).abs(). From the moment you called the method abs, which even exists for multiple integer types, the compiler saw the integer as having an over-constrained type, and aligned with that reasoning, a static type error occurred. The compiler provides a different explanation for this, which is embodied in error E0689.

A method was called on an ambiguous numeric type.

In any case, the real reason why that code does not compile is because the compiler could not identify a type for the integer in that particular case, and did not default the integer type to i32. The Rust reference is not a standard reference for the language, and may be either incomplete or contain outdated information. Other than the misleading concept of over-constraining and under-constraining, the description in the reference matches the compiler's behavior at the time of writing. At this point, we may well be dealing with just another compiler nuance.

See also:

E_net4
  • 27,810
  • 13
  • 101
  • 139
  • It's not entirely clear to me how calling a method overconstrains the type, since that basically put no constrains over that type. – jthulhu Aug 26 '22 at 14:55
  • That refers to the to-be-inferred integer type, @BlackBeans. The constraint was added from the moment that we required that call to have a method called `abs`. – E_net4 Aug 26 '22 at 15:46
  • That somehow makes sense, but in the same time I feel the phrasing is very poor. I would never have understood that just by reading it. – jthulhu Aug 26 '22 at 15:48
  • 1
    @BlackBeans Well, I suppose that gives this Q&A some value, and that one may seek to suggest changes to the [reference page](https://github.com/rust-lang/reference) for clarity. – E_net4 Aug 26 '22 at 16:01
  • @E_net4thecommentflagger and @BlackBeans, thank you for answering and discussing my question, but it is still unclear to me why having a method called ```abs``` "over-constrains" the type of "-100"; "-100_i32" suffices the constraint. Could you show some examples of similar "over-constraints"? – Hiroto Kagotani Aug 27 '22 at 07:36
  • @HirotoKagotani If I understood correctly, overcontraints *in this case* means calling a method. The actual [error message](https://doc.rust-lang.org/stable/error-index.html#E0689) is more precise about what is considered overconstraints. – jthulhu Aug 27 '22 at 09:05
  • Thank you very much for detailed examples and explanations. Signed trait case is very clear; -5 is inferred as ```i32```. Foo trait case is also clear; since Foo is implemented for ```u8``` and ```u16```, the type of 5 is still ambiguous but inferred 5_i32 does not match any implementation. And, OK, type inference for method call is different from function arguments. This is my last question. if there are methods implemented only for ```i32``` and ```i16```, say ```i32_abs``` and ```i16_abs```, are ```(-100).i32_abs()``` and ```(-100).i16_abs()``` legal? – Hiroto Kagotani Aug 28 '22 at 11:59
  • @HirotoKagotani Yes [it is possible](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a616ee3cac78d2c904f8b7ea31b85595). – cpprust Mar 29 '23 at 02:37
  • Why is rust `abs` not implement like [this](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=759a73d6139c1d4a1393ed5eb426b86b)? – cpprust Mar 29 '23 at 02:49
  • 1
    @cpprust That is kind of [what num_traits does](https://docs.rs/num-traits/0.2.15/num_traits/sign/trait.Signed.html), for what it's worth. The original `std` design may have felt that it was not worth exposing `.abs` behind a trait. – E_net4 Mar 29 '23 at 12:47
  • @E_net4 Did it have a cost to exposing `.abs` behind a trait? Why is it not worth? – cpprust Mar 29 '23 at 12:58
  • 1
    @cpprust For one, it would require importing the trait in order to have access to the method. This could be mitigated by including it in the core prelude, although that may also be a change not to be admitted lightly. But trying to think of the underlying reason for this design amounts to a combination of speculation and historical reasons. The multiple developers of the Rust programming language do not claim to have nailed the design behind `std` (or any other aspect of the language) perfectly. – E_net4 Mar 29 '23 at 14:05
  • I don't really understand this answer. Absent any other info, `let x = -100;` will infer `x: i32`. And `std::convert::identity(-100)` will take and return an `i32`. So why doesn't `abs` do the same? What about method calls is different from static function calls? – BallpointBen Mar 30 '23 at 18:11
  • @BallpointBen The last sentence should cover that concern. It is probably just a compiler nuance which prevents it from applying the integer default in the case of these method calls. – E_net4 Mar 30 '23 at 18:40