learn.sol

Rust Enums and Pattern Matching (Option, Result, match)

Learn Rust enums and pattern matching with practical examples, from variant data to real-world use of match, Option, and Result widely used in Solana code.

Today's Mission: Master Data Variants

Learn Rust's most powerful feature for handling different types of data safely. Enums and pattern matching are everywhere in Solana development - from account types to transaction results.

🎲 Understanding Enums in Rust

Enums allow you to define types that can be one of several variants. They're much more powerful than enums in other languages.

Basic Enum Syntax

// Simple enum with unit variants
#[derive(Debug)]
enum Direction {
    North,
    South,
    East,
    West,
}

// Enum with associated data
#[derive(Debug)]
enum OrderStatus {
    Pending,
    Processing { estimated_days: u8 },
    Shipped { tracking_number: String },
    Delivered { signature: String },
    Cancelled { reason: String },
}

fn main() {
    let direction = Direction::North;
    println!("{:?}", direction); // North
    
    let order = OrderStatus::Shipped {
        tracking_number: "1Z999AA1234567890".to_string(),
    };
    println!("{:?}", order);
}

Enums with Different Data Types

#[derive(Debug)]
enum Message {
    Quit,                           // No data
    Move { x: i32, y: i32 },       // Struct-like
    Write(String),                  // Tuple-like
    ChangeColor(i32, i32, i32),    // Multiple values
}

impl Message {
    fn process(&self) {
        match self {
            Message::Quit => println!("Quitting application"),
            Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
            Message::Write(text) => println!("Writing: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!("Changing color to RGB({}, {}, {})", r, g, b)
            }
        }
    }
}

fn main() {
    let messages = vec![
        Message::Move { x: 10, y: 20 },
        Message::Write("Hello, Solana!".to_string()),
        Message::ChangeColor(255, 0, 128),
        Message::Quit,
    ];
    
    for message in messages {
        message.process();
    }
}

🛡️ The Option<T> Enum

The Option<T> enum handles the concept of "something or nothing" safely.

Working with Option<T>

fn find_user_by_id(id: u32) -> Option<String> {
    let users = vec![
        (1, "Alice".to_string()),
        (2, "Bob".to_string()),
        (3, "Charlie".to_string()),
    ];
    
    for (user_id, name) in users {
        if user_id == id {
            return Some(name);
        }
    }
    None
}

fn main() {
    // Different ways to handle Option
    let user = find_user_by_id(2);
    
    // Method 1: Pattern matching with match
    match user {
        Some(name) => println!("Found user: {}", name),
        None => println!("User not found"),
    }
    
    // Method 2: if let syntax
    let user = find_user_by_id(4);
    if let Some(name) = user {
        println!("Found user: {}", name);
    } else {
        println!("User not found");
    }
    
    // Method 3: Using Option methods
    let user = find_user_by_id(1);
    let greeting = user
        .map(|name| format!("Hello, {}!", name))
        .unwrap_or("Hello, stranger!".to_string());
    println!("{}", greeting);
}

Option Methods and Patterns

fn demonstrate_option_methods() {
    let some_number = Some(5);
    let no_number: Option<i32> = None;
    
    // is_some() and is_none()
    println!("some_number is some: {}", some_number.is_some());
    println!("no_number is none: {}", no_number.is_none());
    
    // unwrap_or() - provide default value
    println!("Value or default: {}", no_number.unwrap_or(0));
    
    // unwrap_or_else() - compute default value
    println!("Value or computed: {}", no_number.unwrap_or_else(|| {
        println!("Computing default value...");
        42
    }));
    
    // map() - transform the value if present
    let doubled = some_number.map(|x| x * 2);
    println!("Doubled: {:?}", doubled);
    
    // and_then() - chain operations
    let result = some_number
        .and_then(|x| if x > 0 { Some(x * 2) } else { None })
        .and_then(|x| if x < 20 { Some(x + 1) } else { None });
    println!("Chained operations: {:?}", result);
}

⚡ The Result<T, E> Enum

Result<T, E> handles operations that can succeed or fail.

Basic Result Usage

use std::fs::File;
use std::io::Read;

fn read_file_contents(filename: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>()
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    // Handling different Result types
    match divide(10.0, 2.0) {
        Ok(result) => println!("10 / 2 = {}", result),
        Err(error) => println!("Error: {}", error),
    }
    
    match parse_number("42") {
        Ok(number) => println!("Parsed number: {}", number),
        Err(error) => println!("Parse error: {}", error),
    }
    
    // Using the ? operator for error propagation
    match read_file_contents("example.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(error) => println!("Failed to read file: {}", error),
    }
}

Custom Error Types

#[derive(Debug)]
enum CalculatorError {
    DivisionByZero,
    InvalidOperation,
    Overflow,
}

fn safe_divide(a: i32, b: i32) -> Result<i32, CalculatorError> {
    if b == 0 {
        return Err(CalculatorError::DivisionByZero);
    }
    
    match a.checked_div(b) {
        Some(result) => Ok(result),
        None => Err(CalculatorError::Overflow),
    }
}

fn calculator_operation(op: &str, a: i32, b: i32) -> Result<i32, CalculatorError> {
    match op {
        "add" => a.checked_add(b).ok_or(CalculatorError::Overflow),
        "sub" => a.checked_sub(b).ok_or(CalculatorError::Overflow),
        "mul" => a.checked_mul(b).ok_or(CalculatorError::Overflow),
        "div" => safe_divide(a, b),
        _ => Err(CalculatorError::InvalidOperation),
    }
}

🎯 Pattern Matching with match

The match expression is Rust's most powerful control flow construct.

Comprehensive Match Patterns

#[derive(Debug)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String), // Quarter from a specific state
}

fn coin_value(coin: &Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("Quarter from {}!", state);
            25
        }
    }
}

fn analyze_number(x: i32) {
    match x {
        // Exact values
        0 => println!("Zero"),
        1 => println!("One"),
        
        // Ranges
        2..=10 => println!("Small number (2-10)"),
        11..=100 => println!("Medium number (11-100)"),
        
        // Guards
        n if n < 0 => println!("Negative number: {}", n),
        n if n > 1000 => println!("Large number: {}", n),
        
        // Catch-all
        _ => println!("Some other number: {}", x),
    }
}

fn main() {
    let coins = vec![
        Coin::Penny,
        Coin::Nickel,
        Coin::Quarter("Alaska".to_string()),
        Coin::Dime,
    ];
    
    for coin in &coins {
        println!("{:?} = {} cents", coin, coin_value(coin));
    }
    
    // Pattern matching with numbers
    for number in [0, 5, 15, -5, 2000] {
        analyze_number(number);
    }
}

Advanced Pattern Matching

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Debug)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

fn analyze_point(point: &Point) {
    match point {
        Point { x: 0, y: 0 } => println!("Origin"),
        Point { x: 0, y } => println!("On Y-axis at y = {}", y),
        Point { x, y: 0 } => println!("On X-axis at x = {}", x),
        Point { x, y } if x == y => println!("On diagonal at ({}, {})", x, y),
        Point { x, y } => println!("Point at ({}, {})", x, y),
    }
}

fn calculate_area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle { base, height } => 0.5 * base * height,
    }
}

fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 5, y: 0 },
        Point { x: 0, y: 3 },
        Point { x: 4, y: 4 },
        Point { x: 2, y: 7 },
    ];
    
    for point in &points {
        analyze_point(point);
    }
    
    let shapes = vec![
        Shape::Circle { radius: 5.0 },
        Shape::Rectangle { width: 4.0, height: 3.0 },
        Shape::Triangle { base: 6.0, height: 4.0 },
    ];
    
    for shape in &shapes {
        println!("{:?} has area: {:.2}", shape, calculate_area(shape));
    }
}

🎈 Control Flow with if let

if let provides a concise way to handle specific patterns.

fn process_config_value(config: Option<String>) {
    // Instead of this verbose match:
    // match config {
    //     Some(value) => println!("Config: {}", value),
    //     None => {}
    // }
    
    // Use if let for cleaner code:
    if let Some(value) = config {
        println!("Config: {}", value);
    }
}

fn handle_operation_result(result: Result<i32, String>) {
    if let Ok(value) = result {
        println!("Operation succeeded: {}", value);
    } else if let Err(error) = result {
        println!("Operation failed: {}", error);
    }
}

// while let for iterating until pattern fails
fn process_stack(mut stack: Vec<i32>) {
    while let Some(value) = stack.pop() {
        println!("Processing: {}", value);
        
        // Break on specific condition
        if value == 0 {
            break;
        }
    }
}

fn main() {
    process_config_value(Some("debug".to_string()));
    process_config_value(None);
    
    handle_operation_result(Ok(42));
    handle_operation_result(Err("Invalid input".to_string()));
    
    let stack = vec![1, 2, 3, 0, 4, 5];
    process_stack(stack);
}

🔧 Practical Example: Token Management System

Let's build a token management system that demonstrates all enum concepts:

use std::collections::HashMap;

// Token status enum
#[derive(Debug, Clone, PartialEq)]
enum TokenStatus {
    Active { supply: u64 },
    Paused { reason: String },
    Frozen { until_block: u64 },
    Burned,
}

// Token metadata enum
#[derive(Debug, Clone)]
enum TokenMetadata {
    Standard {
        decimals: u8,
        description: String,
    },
    NFT {
        image_url: String,
        attributes: HashMap<String, String>,
    },
    Utility {
        purpose: String,
        burn_rate: f64,
    },
}

// Custom error enum
#[derive(Debug)]
enum TokenError {
    NotFound(u32),
    InvalidStatus { expected: String, found: String },
    InsufficientSupply { requested: u64, available: u64 },
    PermissionDenied,
}

// Token struct
#[derive(Debug, Clone)]
struct Token {
    id: u32,
    symbol: String,
    status: TokenStatus,
    metadata: TokenMetadata,
}

impl Token {
    fn new(id: u32, symbol: String, metadata: TokenMetadata) -> Self {
        Self {
            id,
            symbol,
            status: TokenStatus::Active { supply: 0 },
            metadata,
        }
    }
    
    fn is_active(&self) -> bool {
        matches!(self.status, TokenStatus::Active { .. })
    }
    
    fn get_supply(&self) -> Option<u64> {
        match &self.status {
            TokenStatus::Active { supply } => Some(*supply),
            _ => None,
        }
    }
    
    fn mint(&mut self, amount: u64) -> Result<(), TokenError> {
        match &mut self.status {
            TokenStatus::Active { supply } => {
                *supply += amount;
                Ok(())
            }
            status => Err(TokenError::InvalidStatus {
                expected: "Active".to_string(),
                found: format!("{:?}", status),
            }),
        }
    }
    
    fn burn(&mut self, amount: u64) -> Result<(), TokenError> {
        match &mut self.status {
            TokenStatus::Active { supply } => {
                if *supply >= amount {
                    *supply -= amount;
                    if *supply == 0 {
                        self.status = TokenStatus::Burned;
                    }
                    Ok(())
                } else {
                    Err(TokenError::InsufficientSupply {
                        requested: amount,
                        available: *supply,
                    })
                }
            }
            status => Err(TokenError::InvalidStatus {
                expected: "Active".to_string(),
                found: format!("{:?}", status),
            }),
        }
    }
    
    fn pause(&mut self, reason: String) -> Result<(), TokenError> {
        if self.is_active() {
            self.status = TokenStatus::Paused { reason };
            Ok(())
        } else {
            Err(TokenError::InvalidStatus {
                expected: "Active".to_string(),
                found: format!("{:?}", self.status),
            })
        }
    }
    
    fn freeze(&mut self, until_block: u64) -> Result<(), TokenError> {
        if self.is_active() {
            self.status = TokenStatus::Frozen { until_block };
            Ok(())
        } else {
            Err(TokenError::InvalidStatus {
                expected: "Active".to_string(),
                found: format!("{:?}", self.status),
            })
        }
    }
    
    fn resume(&mut self, current_block: u64) -> Result<(), TokenError> {
        match &self.status {
            TokenStatus::Paused { .. } => {
                self.status = TokenStatus::Active { supply: 0 };
                Ok(())
            }
            TokenStatus::Frozen { until_block } => {
                if current_block >= *until_block {
                    self.status = TokenStatus::Active { supply: 0 };
                    Ok(())
                } else {
                    Err(TokenError::PermissionDenied)
                }
            }
            _ => Err(TokenError::InvalidStatus {
                expected: "Paused or Frozen".to_string(),
                found: format!("{:?}", self.status),
            }),
        }
    }
}

// Token manager
struct TokenManager {
    tokens: Vec<Token>,
    next_id: u32,
}

impl TokenManager {
    fn new() -> Self {
        Self {
            tokens: Vec::new(),
            next_id: 1,
        }
    }
    
    fn create_token(&mut self, symbol: String, metadata: TokenMetadata) -> u32 {
        let id = self.next_id;
        self.next_id += 1;
        
        let token = Token::new(id, symbol, metadata);
        self.tokens.push(token);
        
        id
    }
    
    fn get_token(&self, id: u32) -> Result<&Token, TokenError> {
        self.tokens.iter()
            .find(|token| token.id == id)
            .ok_or(TokenError::NotFound(id))
    }
    
    fn get_token_mut(&mut self, id: u32) -> Result<&mut Token, TokenError> {
        self.tokens.iter_mut()
            .find(|token| token.id == id)
            .ok_or(TokenError::NotFound(id))
    }
    
    fn mint_tokens(&mut self, id: u32, amount: u64) -> Result<(), TokenError> {
        let token = self.get_token_mut(id)?;
        token.mint(amount)
    }
    
    fn burn_tokens(&mut self, id: u32, amount: u64) -> Result<(), TokenError> {
        let token = self.get_token_mut(id)?;
        token.burn(amount)
    }
    
    fn list_active_tokens(&self) -> Vec<&Token> {
        self.tokens.iter()
            .filter(|token| token.is_active())
            .collect()
    }
    
    fn get_total_supply(&self) -> u64 {
        self.tokens.iter()
            .filter_map(|token| token.get_supply())
            .sum()
    }
}

fn main() {
    let mut manager = TokenManager::new();
    
    // Create different types of tokens
    let sol_id = manager.create_token(
        "SOL".to_string(),
        TokenMetadata::Standard {
            decimals: 9,
            description: "Solana native token".to_string(),
        },
    );
    
    let nft_id = manager.create_token(
        "MONKEY".to_string(),
        TokenMetadata::NFT {
            image_url: "https://example.com/monkey.png".to_string(),
            attributes: {
                let mut attrs = HashMap::new();
                attrs.insert("rarity".to_string(), "rare".to_string());
                attrs.insert("background".to_string(), "blue".to_string());
                attrs
            },
        },
    );
    
    // Mint some tokens
    match manager.mint_tokens(sol_id, 1000000) {
        Ok(()) => println!("Minted SOL tokens successfully"),
        Err(e) => println!("Failed to mint SOL: {:?}", e),
    }
    
    // Try operations and handle results
    match manager.mint_tokens(nft_id, 1) {
        Ok(()) => println!("Minted NFT successfully"),
        Err(e) => println!("Failed to mint NFT: {:?}", e),
    }
    
    // Pause a token
    if let Ok(token) = manager.get_token_mut(sol_id) {
        if let Err(e) = token.pause("Maintenance".to_string()) {
            println!("Failed to pause token: {:?}", e);
        } else {
            println!("Token paused for maintenance");
        }
    }
    
    // List active tokens
    println!("\nActive tokens:");
    for token in manager.list_active_tokens() {
        println!("- {} (ID: {}): {:?}", token.symbol, token.id, token.status);
    }
    
    println!("Total supply across all active tokens: {}", manager.get_total_supply());
}

🎯 Day 4 Practice Exercises

Exercise 1: Traffic Light System

Create a traffic light controller with states and transitions:

#[derive(Debug, PartialEq)]
enum TrafficLight {
    Red { duration: u32 },
    Yellow { duration: u32 },
    Green { duration: u32 },
}

impl TrafficLight {
    fn next(&self) -> TrafficLight {
        // Implement state transitions
        todo!()
    }
    
    fn time_remaining(&self) -> u32 {
        // Return remaining time
        todo!()
    }
    
    fn can_go(&self) -> bool {
        // Return true if traffic can proceed
        todo!()
    }
}

Exercise 2: Configuration Parser

Build a configuration system that handles different value types:

#[derive(Debug)]
enum ConfigValue {
    String(String),
    Integer(i64),
    Boolean(bool),
    Array(Vec<ConfigValue>),
}

fn parse_config(input: &str) -> Result<ConfigValue, String> {
    // Parse string input into ConfigValue
    todo!()
}

Exercise 3: Simple Calculator

Create a calculator that handles different operations and errors:

#[derive(Debug)]
enum Operation {
    Add(f64, f64),
    Subtract(f64, f64),
    Multiply(f64, f64),
    Divide(f64, f64),
    Power(f64, f64),
}

#[derive(Debug)]
enum CalculatorError {
    DivisionByZero,
    InvalidInput,
    Overflow,
}

fn calculate(op: Operation) -> Result<f64, CalculatorError> {
    // Implement calculator logic
    todo!()
}

🚀 What's Next?

You've now mastered:

  • Enum definitions and variants
  • Option<T> and Result<T, E> handling
  • Pattern matching with match
  • Control flow with if let and while let
  • Custom error types

Tomorrow we'll dive into Collections and String Handling - the tools you need to manage data efficiently in Solana programs!

Solana Connection

Enums are heavily used in Solana development: account types, instruction variants, program errors, and state transitions. The patterns you learned today will appear in every Anchor program you write!

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Rust Enums and Pattern Matching (Option, Result, match) | learn.sol