Basic Types

In R, everything is a vector or a collection of values. There is no concept of a single value variable or scalar. The closest equivalent to that is a length-one vector. In Rust, however, scalars are the building blocks of everything. When a collection is needed, it is made directly from scalars. For this reason, you will often hear them referred to as primitives. A primitive or scalar can be used to hold a single value of a specific data type, including strings, integers, and floats, among others. In this tutorial, we will cover some of those data types, focusing on how to instantiate them and convert between them.

Integers in Rust are a good place to start. They can be either signed, meaning they can represent negative values, or unsigned, meaning they support only positive values. Both varieties come in several sizes depending on how many bits are needed to store the value:

The letter prefix indicates the type (i for signed, u for unsigned) and the number indicates the bit width. Unsigned integers can hold larger positive values than their signed counterparts because they do not need to use a bit to represent a negative value:

i32::MAX // 2147483647
u32::MAX // 4294967295

Floating points are similar to integers but can represent decimal values, too. Unlike integers, floating points are also always signed and come in two sizes: f32 and f64.

Note

In R, integer vectors are comprised of only i32 values, and floating points, called doubles, are always f64 values.

Rust can usually infer types. For instance, it can distinguish the integer 1 from the floating point 1.0, but you can also specify the type explicitly. There are two ways to do this — using : in the assignment, or by appending a type suffix to the literal value:

let x: f64 = 10.0;
let x = 10.0f64;

You can also use _ as a visual separator when writing numeric literals. The following are all identical:

let x: i32 = 1000;
let x = 1000i32;
let x = 1_000_i32;

This pattern of explicitly declaring the type of a variable may seem somewhat verbose to the average R user since R is very lax about types, relying as it does on a lot of implicit conversions. Becuase of Rust’s memory management model, however, Rust is a strongly, statically typed language. That means that all types must be known when you go to build or compile your crate. It also precludes running operations on different types. For example, math expressions can only be performed between values of the same type, so you cannot, for example, directly add an f64 and an i32. You must first cast one to match the type of the other, in the simplest case using the as keyword:

fn add2(x: f64, y: i32) -> f64 {
    x + (y as f64)
}

If you call add2(3.2, 2), the function will first cast y to f64 before doing addition, returning the f64 value 5.2. Before we continue, note that Rust supports all the same numeric operators as R: +, -, /, *, and % for remainder (equivalent to %% in R).