Enum

1. Welcome and Icebreaker:

Welcome back!

If you could create a new enum to represent different states of matter (beyond solid, liquid, gas, plasma), what would its variants be, and what properties would each state have?

2. Outline the Session:

Here’s the plan:

  1. Concept Explanation: We’ll define enums, their syntax, and how they differ from enums in other languages. We’ll then introduce the Option enum and explain how it represents the presence or absence of a value.
  2. Live Coding Demonstration: We’ll write code to demonstrate how to define enums, use them with match, create Option values, and handle Some and None cases.
  3. Tasks: You’ll get exercises to practice defining your own enums and working with Option.

Learning Outcomes: By the end of this session, you will be able to:

  • Define and use custom enums.
  • Understand how enums can hold associated data.
  • Use match effectively with enums.
  • Understand the purpose and use of the Option type.
  • Handle Some and None variants of Option correctly.
  • Use methods like unwrap(), unwrap_or(), is_some(), and is_none().

Expectations: Enums and Option are fundamental to Rust’s type system and safety guarantees. Pay close attention to how they’re used in conjunction with match.

3. Concept Explanation:

Enums (Enumerations):

  • Enums allow you to define a type by enumerating its possible variants.

  • Unlike C-style enums, Rust enums can hold associated data, making them much more versatile.

  • Syntax:

    enum MyEnum {
        Variant1,
        Variant2(i32), // Variant with an associated i32 value
        Variant3 { x: i32, y: String }, // Variant with named fields
    }
    
  • Each variant can have:

    • No associated data.
    • One or more associated values (like a tuple).
    • Named fields (like a struct).

The Option Type:

  • Option is a predefined enum in the Rust standard library that represents the possibility of a value being either present or absent.

  • Definition:

    enum Option<T> {
        Some(T), // Represents the presence of a value of type T
        None,    // Represents the absence of a value
    }
    
  • Option<T> is a generic type, where T can be any type.

  • Option is used extensively in Rust to handle situations where a value might not exist, avoiding null pointer errors.

  • It forces you to explicitly consider the case where a value is missing.

4. Live Coding Demonstration:

// A simple enum
enum Direction {
    North,
    South,
    East,
    West,
}

// Enum with associated data
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

// Using Option
fn find_element(arr: &[i32], target: i32) -> Option<usize> {
    for (index, &value) in arr.iter().enumerate() {
        if value == target {
            return Some(index); // Found the element, return its index
        }
    }
    None // Element not found
}

fn main() {
    // Using the Direction enum
    let my_direction = Direction::North;

    match my_direction {
        Direction::North => println!("Going North"),
        Direction::South => println!("Going South"),
        Direction::East => println!("Going East"),
        Direction::West => println!("Going West"),
    }

    // Using the Message enum
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: -5 };
    let msg3 = Message::Write(String::from("Hello"));

    match msg2 {
        Message::Quit => println!("Quit message"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b),
    }

    // Using the Option type
    let numbers = [1, 2, 3, 4, 5];
    let result = find_element(&numbers, 3);

    match result {
        Some(index) => println!("Element found at index: {}", index),
        None => println!("Element not found"),
    }

    // --- Option methods ---

    let maybe_number: Option<i32> = Some(42);
    let definitely_number = maybe_number.unwrap(); // Use with caution! Panics if None
    println!("Definitely: {}", definitely_number);

    let maybe_number2: Option<i32> = None;
    let default_number = maybe_number2.unwrap_or(0); // Provides a default value if None
    println!("Default: {}", default_number);

    println!("Is some: {}", maybe_number.is_some());
    println!("Is none: {}", maybe_number2.is_none());

     // Using if let for concise Option handling
    if let Some(value) = maybe_number {
        println!("if let Some: {}", value);
    }

    // Using while let (less common, but useful for loops)
    let mut optional_value = Some(0);
    while let Some(value) = optional_value {
        if value > 5 {
            println!("Reached limit!");
            optional_value = None;
        } else {
            println!("Value: {}", value);
            optional_value = Some(value + 1);
        }
    }
}

Key Takeaways:

  • Enum Variants: We define enums with different kinds of variants, including those with associated data.
  • match with Enums: match is used to handle different enum variants, including destructuring associated data.
  • Option: The find_element function demonstrates how Option is used to return a value that might or might not be present.
  • Option Methods: We show how to use unwrap(), unwrap_or(), is_some(), and is_none() to work with Option values. unwrap() should be used with caution, as it will panic if the Option is None.
  • if let and while let: These provide concise ways to handle Some cases without full match expressions.

5. Tasks

  1. Traffic Light Enum: Create an enum called TrafficLight with variants Red, Yellow, and Green. Write a function that takes a TrafficLight as input and prints a message indicating what action to take (e.g., “Stop” for Red).

  2. Shape Enum: Create an enum called Shape with variants representing different shapes:

    • Circle(f64): Represents a circle with a radius (the associated f64 value).
    • Rectangle { width: f64, height: f64 }: Represents a rectangle with width and height.
    • Triangle(f64, f64, f64): Represents a triangle with three sides. Write a function called area that takes a Shape as input and returns the area of the shape as an f64.
  3. Option and Division: Write a function called safe_divide that takes two f64 numbers as input and returns an Option<f64>. If the second number is zero, the function should return None. Otherwise, it should return Some(result), where result is the result of the division.

  4. Option and String Parsing: Write a function called parse_number that takes a string slice (&str) as input and returns an Option<i32>. The function should attempt to parse the string as an integer. If the parsing is successful, return Some(number). If the parsing fails (e.g., the string contains non-numeric characters), return None. (Hint: Use the .parse() method on the string slice, which returns a Result<i32, _>. You can then use .ok() on the Result to convert it to an Option<i32>).

  5. unwrap_or_else: Use the unwrap_or_else method on an Option to provide a default value that is computed by a closure (a short, anonymous function). For example:

    let my_option: Option<i32> = None;
    let value = my_option.unwrap_or_else(|| {
        // Some complex calculation to determine the default value
        println!("Calculating default...");
        42
    });
    println!("Value: {}", value);
    
  6. Chaining Options: Use the and_then method to chain together multiple operations that might return None. For example:

        fn get_user_id() -> Option<i32> {
            // Simulate fetching user ID (might return None)
            Some(123)
        }
    
        fn get_username(id: i32) -> Option<String> {
            //Simulate fetching username
            Some("username".to_string())
        }
        let username = get_user_id().and_then(get_username);
    
        match username {
            Some(name) => println!("Username: {}", name),
            None => println!("Could not retrieve username"),
        }
    
  7. map on Option: Use the map method to transform the value inside a Some variant without needing a full match expression. For instance, if you have an Option<i32>, you can use map to double the value inside the Some, leaving None unchanged.

    let maybe_number: Option<i32> = Some(5);
    let doubled_option = maybe_number.map(|x| x * 2);
    
    println!("Original: {:?}", maybe_number); // Prints: Original: Some(5)
    println!("Doubled: {:?}", doubled_option);   // Prints: Doubled: Some(10)
    
    let no_number: Option<i32> = None;
    let doubled_none = no_number.map(|x| x * 2);
    println!("Doubled None: {:?}", doubled_none); // Prints: Doubled None: None