Numbers

In Perl, something like "3" + 4 works because the + operator places each of its arguments in numeric context. In Rust, the types of the operands must match because there's a different + operator for each type.

In Perl, the operators are in charge. In Rust, the operands are. (More specifically, the types of the operands.)

And we don't just mean strings and numbers; Rust has lots of different numeric types. There are signed integers, unsigned integers, and floating-point numbers, each in various sizes. We cannot do arithmetic until all the types are the same.

The names of the types are letters i, u, and f followed by the number of bits. There are also types for the native size on the current platform. So we have

signed integers: i8, i16, i32, i64, i128, isize
unsigned integers: u8, u16, u32, u64, u128, usize
floating-point numbers: f32, f64

That's fourteen kinds of numbers, each with its own + operation. Yikes!

In practice, it's not as bad as you might think. This Perl code

    my $x = 3;
    my $y = 4;

    my $z = $x + $y;

    say $z;

looks nearly the same in Rust


#![allow(unused_variables)]
fn main() {
    let x = 3;
    let y = 4;

    let z = x + y;

    println!("{}", z);
}

We have let instead of my and all of the dollar signs are missing, but that's about it. So where are all the types we were so worried about? In Perl, the dollar signs at least tell us we have scalars. In Rust, we get nothing. In fact, everything is implied. First, those numeric literals (the 3 and 4) have defaults. Since we didn't specify, Rust assumes they are 32-bit signed integers. We could have written them with the type appended, like so.


#![allow(unused_variables)]
fn main() {
    let x = 3i32;
    let y = 4i32;
}

From there, the types of x and y are inferred. Since we're stuffing i32's in them, they must be of type i32. And then the type of z is inferred. Summing two i32's gives another i32 and we're stuffing that in z, so z must be of type i32.

Incidentally, we can put underscores in numeric literals anywhere we want, just as in Perl. That's handy for big numbers, like 1_000_000 in both Perl and Rust. But in Rust, it's also common to use it for these type annotations.


#![allow(unused_variables)]
fn main() {
    let x = 3_i32;
    let y = 4_i32;
}

Alternatively, we could specify the types of the variables, like so.


#![allow(unused_variables)]
fn main() {
    let x: i32 = 3;
    let y: i32 = 4;
}

We could even do both, but that's starting to look silly.


#![allow(unused_variables)]
fn main() {
    let x: i32 = 3_i32;
    let y: i32 = 4_i32;
}

If x and y were two different types, then trying to add them would be an error.


#![allow(unused_variables)]
fn main() {
    let x: i32 = 3;
    let y: i64 = 4;

    let z = x + y;
}

Note that this is a compile-time error; we never get a chance to run this. Here is some of the compiler output

...
error[E0308]: mismatched types
  --> src/main.rs:12:17
   |
12 |     let z = x + y;
   |                 ^ expected `i32`, found `i64`

error[E0277]: cannot add `i64` to `i32`
  --> src/main.rs:12:15
   |
12 |     let z = x + y;
   |               ^ no implementation for `i32 + i64`
   |
   = help: the trait `std::ops::Add<i64>` is not implemented for `i32`

error: aborting due to 2 previous errors
...

One way to remedy this would be to use the as keyword to coerce one of the types into the other.


#![allow(unused_variables)]
fn main() {
    let x: i32 = 3;
    let y: i64 = 4;

    let z = (x as i64) + y;
}

Here, z would be inferred to be an i64, as it's the sum of two i64s. This is perfectly safe, as every i32 is expressible as an i64. If we went the other way, namely


#![allow(unused_variables)]
fn main() {
    let z = x + (y as i32);
}

then we have to be a bit careful. In this case, we're fine because 4 is obviously expressible as an i32. But not every i64 is expressible as an i32. And the as keyword is naïve, so it could quietly give us the wrong answer. We will say more about this when we discuss error handling.