Lecture 7 - Variables and types

Logistics

  • HW2 due next Wednesday
  • I have office hours today

Circling back on comments

You definitely can make multi-line comments

#![allow(unused)]
fn main() {
// This is a single-line comment
let age = 25; // Comments can go at the end of lines

/*
   This is a multi-line comment
   Useful for longer explanations
   or temporarily disabling code
*/

/// This is a line doc comment that sits on the OUTSIDE
/** Or as a block comment (note the one ending asterisk) */
fn my_function(){
    //! This is a line doc comment that sits on the INSIDE
    /*! this is a block doc comment that sits on the inside */

}
}

Learning Objectives

  • Use the mut keyword and shadowing with let to modify variables
  • Declare constants using const
  • Use Rust's integer types and floating-point types and understand their ranges
  • Understand rust's basic types and their sizes (ints, floats,bool, char, &str)
  • Use type annotation (with let) and type conversion (as)
  • Work with boolean values using logical operators (&&, ||, !)
  • Create, access, and destructure tuples

Variables and Mutability

Variables are by default immutable!

Let's try this and then fix it.

fn main(){
    let x = 3;
    x = x + 1; 
    println!("{x}")
}

Why can't we do this now?

fn main(){
    let mut x = 3;
    x = 9.5;
    println!("{x}")
}

One way to fix - Variable shadowing: new variable with the same name

fn main(){
    let solution = "4";
    let solution : i32 = solution.parse()
                        .expect("Not a number!");
    let solution = solution * (solution - 1) / 2;
    println!("solution = {}",solution);
    let solution = "This is a string";
    println!("solution = {}", solution);
}

Why does this work even though solution isn't mutable?

Variables vs Constants

Sometimes you need values that never change and are known at compile time:

#![allow(unused)]
fn main() {
const MAX_PLAYERS: u32 = 100;
const PI: f64 = 3.14159;
const GREETING: &str = "Hello, world!";
}

Constants:

  • Are always immutable (no mut allowed)
  • Use const instead of let
  • Must have explicit types
  • Named in ALL_UPPERCASE by convention
  • Can be declared in any scope (including global)
  • Must be computable at compile-time (so typically hard-coded)

When to use constants vs variables:

  • Constants: Mathematical constants, configuration values, limits
  • Variables: Data that might change or is computed at runtime

Types - Integers

Binary representations

Representing 13:

  • In decimal (base 10): 13 = 1×10¹ + 3×10⁰
  • In binary (base 2): 1101 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13

For example, the number 13 in binary is 1101:

Binary:   1 1 0 1
Position: 3 2 1 0
2x^n:     8 4 2 1
Value:    8 4 0 1  → 8+4+1 = 13

T/P/S - What's the largest integer we can represent with 4 binary digits?

So what are ints, under the hood

Unsigned integers are stored in binary format.

But (signed) integers are stored in two's complement format, where:

  • if the number is positive, the first bit is 0
  • if the number is negative, the first bit is 1

To calculate the two's complement of a negative number, we flip all the bits and add 1.

#![allow(unused)]
fn main() {
// binary representation of 7 and -7
println!("{:032b}", 7);
println!("{:032b}", -7);
}

(Think/pair/share) Why do you think we do it this way?

Bits and bytes

  • Bit: The smallest unit of data in computing - can store either 0 or 1
  • Byte: A group of 8 bits - the basic addressable unit of memory
  • Why 8 bits? 8 bits can represent 2⁸ = 256 different values (0-255)
  • Computers typically address memory in byte-sized chunks
  • (In sizes like "16 GB of RAM" GB refers to "gigaBYTES" not gigaBITS)

Integers come in all shapes and sizes

  • unsigned integers: u8, u16, u32, u64, u128, usize (architecture specific size)
    • from \(0\) to \(2^n-1\)
  • signed integers: i8, i16, i32 (default), i64, i128, isize (architecture specific size)
    • from \(-2^{n-1}\) to \(2^{n-1}-1\)

These numbers (like u16) refer to bits, not bytes!

if you need to convert, use the as operator

i128 and u128 are useful for cryptography

Let's try it - min and max values of int types

#![allow(unused)]
fn main() {
println!("U8 min is {} max is {}", u8::MIN, u8::MAX);
println!("I8 min is {} max is {}", i8::MIN, i8::MAX);
println!("U16 min is {} max is {}", u16::MIN, u16::MAX);
println!("I16 min is {} max is {}", i16::MIN, i16::MAX);
println!("U32 min is {} max is {}", u32::MIN, u32::MAX);
println!("I32 min is {} max is {}", i32::MIN, i32::MAX);
println!("U64 min is {} max is {}", u64::MIN, u64::MAX);
println!("I64 min is {} max is {}", i64::MIN, i64::MAX);
println!("U128 min is {} max is {}", u128::MIN, u128::MAX);
println!("I128 min is {} max is {}", i128::MIN, i128::MAX);
println!("USIZE min is {} max is {}", usize::MIN, usize::MAX);
println!("ISIZE min is {} max is {}", isize::MIN, isize::MAX);
}

Different types don't play nice together

fn main(){
    let x : i16 = 13;
    let y : i32 = -17;
    println!("{}", x * y);   // will not work
    // println!("{}", (x as i32)* y);
}

Be careful with math on ints

u8 is 8 bits and can store maximum value 2^8 - 1 = 255.

If we multiply: \(255*255=65025\).

How many bits do we need to store this value? We can take the log base 2 of the value.

>>> import math
>>> math.log2(255*255)
15.988706873717716

So we need 16 bits to store the product of two u8 values.

In general when we multiply two numbers of size \(n\) bits, we need \(2n\) bits to store the result.

Types - Floats

Why are they called floats?

  • Two kinds: f32 and f64 (default)
  • What do these mean?

Sizes of floats

#![allow(unused)]
fn main() {
println!("F32 min is {} max is {}", f32::MIN, f32::MAX);
println!("F32 min is {:e} max is {:e}", f32::MIN, f32::MAX);
println!("F64 min is {:e} max is {:e}", f64::MIN, f64::MAX);
}

Why these sizes?

  • f32: 1 sign bit + 8 exponent bits + 23 significance bits
  • f64: 1 sign bit + 11 exponent bits + 52 significance bits

Floats and Rust's type inference system

fn main(){
    let x:f32 = 4.0;
    let y:f32 = 4; // Will not work.  It will not autoconvert for you.

    let z = 1.25; // won't get automatically assigned a type yet

    println!("{:.1}", x * z);

    //println!("{:.1}", (x as f64) * z);
}

Formatting in println! (this didn't make it to your print-outs!)

You can control how numbers are displayed using format specifiers:

#![allow(unused)]
fn main() {
let total = 21.613749999999997;
let big_number = 1_234_567.89;
let small_number = 0.000123;
let count = 42;

// Float formatting
println!("Default: {}", total);         // Default: 21.613749999999997
println!("2 decimals: {:.2}", total);   // 2 decimals: 21.61
println!("Currency: ${:.2}", price);    // Currency: $19.99

// Scientific notation
println!("Scientific: {:e}", big_number);    // Scientific: 1.234568e6
println!("Scientific: {:.2e}", small_number); // Scientific: 1.23e-4

// Integer formatting
println!("Default: {}", count);         // Default: 42
println!("Width 5: {:5}", count);       // Width 5:    42
println!("Zero-pad: {:05}", count);     // Zero-pad: 00042
println!("Binary: {:b}", count);        // Binary: 101010
println!("Hex: {:x}", count);           // Hex: 2a
}

Useful patterns:

  • {:.2} - 2 decimal places
  • {:e} - scientific notation
  • {:5} - fixed width 5
  • {:b} - binary, {:x} - hexadecimal

Mini-Quiz

Take a minute to talk to a partner about what these do, then I'll call on you

  1. cargo new my_project
  2. cargo check
  3. git add .
  4. rustc hello.rs
  5. git pull
  6. cargo run --release
  7. git commit -m "fix bug"

Types - Booleans (and logical operators)

  • bool uses one byte of memory (why not one bit?)
#![allow(unused)]
fn main() {
let x = true;
let y: bool = false;

println!("{}", x && y); // logical and
println!("{}", x || y); // logical or
println!("{}", !y);    // logical not

}

Bitwise operators (just for awareness)

There are also bitwise operators that look similar to logical operators:

#![allow(unused)]
fn main() {
let x = true;
let y: bool = false;
println!("{}", x & y);  // bitwise and
println!("{}", x | y);  // bitwise or
}

But they also work on integers

fn main(){
    let x = 10;
    let y = 7;
    println!("{x:04b} & {y:04b} = {:04b}", x & y);
    println!("{x:04b} | {y:04b} = {:04b}", x | y);
    // println!("{}", x && y);
    // println!("{}", x || y);
}

So the negation of an int is...

#![allow(unused)]
fn main() {
let y = 7;
println!("!{y:04b} = {:04b} or {0}", !y);
}

Think/pair/share - What is this going to print?

#![allow(unused)]
fn main() {
let y:i8 = 7;
println!("{:016b}", y);
println!("{:016b}", !y);
println!("{:016b}", -1*y);
}

Types - Characters

  • char defined via single quotes, uses four bytes of memory (that's how many bits?)
  • For a complete list of UTF-8 characters check https://www.fileformat.info/info/charset/UTF-8/list.htm
#![allow(unused)]
fn main() {
let x: char = 'a';
let y = '🚦';
let z = '🦕';

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

(Fun fact - try Control-Command-Space (Mac) or Windows-Key + . (Windows) to add emojis anywhere!)

Types - Strings

  • A string slice (&str) is defined via double quotes (we'll talk much more about what this means later!)
fn main() {
    let s1 = "Hello! How are you, 🦕?";  // type is immutable borrowed reference to a string slice: `&str`
    let s2 : &str = "Καλημέρα από την Βοστώνη και την DS210";  // here we make the type explicit
    
    println!("{}", s1);
    println!("{}\n", s2);

    // This doesn't work.  You can't do String = &str
    //let s3: String = "Does this work?";
    
    let s3: String = "Does this work?".to_string();
    println!("{}", s3);

    let s4: String = String::from("How about this?");
    println!("{}\n", s4);

    let s5: &str = &s3;
    println!("str reference to a String reference: {}\n", s5);
    
    // This won't work. 
    // println!("{}", s1[3]);
    // println!("{}", s4[3]);

    // But you can index this way.
    println!("4th character of s1: {}", s1.chars().nth(3).unwrap());
    println!("3rd character of s3: {}", s4.chars().nth(2).unwrap());
}

Tuples in Rust

Tuples are a general-purpose data structure that can hold multiple values of different types.

#![allow(unused)]
fn main() {
let mut tuple = (1, 1.1);
let mut tuple2: (i32, f64) = (1, 1.1);  // type annotation is optional

let another = ("abc", "def", "ghi");

let yet_another: (u8, u32) = (255, 4_000_000_000);
}

Accessing elements of a tuple

Rust tuples are "0-based":

#![allow(unused)]
fn main() {
let mut tuple = (1,1.1);
println!("({}, {})", tuple.0, tuple.1);

tuple.0 = 2;

println!("({}, {})",tuple.0,tuple.1);

println!("Tuple is {:?}", tuple);
}

We can unpack a tuple by matching a pattern

#![allow(unused)]
fn main() {
// or pattern match and desconstruct
let mut tuple = (1,1.1);
let (a, b) = tuple;
println!("a = {}, b = {}",a,b);
}

Best Practices for tuples

When to Use Tuples:

  • Small, related data: 2-4 related values
  • Temporary grouping: Short-lived data combinations
  • Function returns: Multiple return values
  • Pattern matching: When destructuring is useful

Style Guidelines:

#![allow(unused)]
fn main() {
// Good: Clear, concise
let (width, height) = get_dimensions();

// Good: Descriptive destructuring
let (min_temp, max_temp, avg_temp) = analyze_temperatures(&data);

// Avoid: Too many elements
// let config = (true, false, 42, 3.14, "test", 100, false);  // Hard to read

// Avoid: Unclear meaning
// let data = (42, 13);  // What do these numbers represent?
}