Advanced Level

Advanced Types

Chapter 20: Advanced Types ๐Ÿงช

Explore sophisticated type system features like type aliases, never type, dynamically sized types, and more.

Type Aliases

Creating Type Synonyms

Type aliases create synonyms for existing types without introducing new types:

type Kilometers = i32;

fn main() {
    let x: i32 = 5;
    let y: Kilometers = 5;
    
    // These are the same type internally
    println!("x + y = {}", x + y);
}

Complex Type Aliases

use std::collections::HashMap;
use std::io::Result;

type Thunk = Box<dyn Fn() + Send + 'static>;
type Result<T> = std::result::Result<T, std::io::Error>;
type Tables = HashMap<String, HashMap<String, String>>;

fn main() {
    let f: Thunk = Box::new(|| println!("hi"));
    let r: Result<i32> = Ok(42);
    let t: Tables = HashMap::new();
}

The Never Type

Representing Unreachable Code

The never type ! represents values that never return:

fn bar() -> ! {
    panic!("This call never returns!");
}

fn some_function() -> Result<i32, String> {
    // This function returns a Result, but we can use ! to handle errors
    let result = match risky_operation() {
        Ok(value) => value,
        Err(_) => bar(), // Never returns, so this branch never continues
    };
    
    // This code is unreachable after bar(), but compiler knows it's safe
    Ok(result)
}

fn risky_operation() -> Result<i32, String> {
    Err(String::from("Error occurred"))
}

Practical Use Cases

fn main() {
    let guess = loop {
        let input = get_input();
        
        match input.parse::<u32>() {
            Ok(num) => break num,
            Err(_) => continue, // continue has type !
        }
    };
    
    println!("You guessed: {}", guess);
}

fn get_input() -> String {
    String::from("42")
}

Dynamically Sized Types

Sized and Unsized Types

Most types in Rust have a known size at compile time, but some don't:

// This won't compile - unsized types can't be stored directly
// let s1: str = "Hello there!";
// let s2: str = "How's it going?";

// This works - storing references to unsized types
let s1: &str = "Hello there!";
let s2: &str = "How's it going?";

Using Box with DSTs

fn main() {
    // This won't compile
    // let s: str = "Hello";
    
    // But this works
    let s: Box<str> = "Hello".to_owned().into_boxed_str();
    println!("{}", s);
}

Working with DSTs

Trait Objects

trait Draw {
    fn draw(&self);
}

struct Circle {
    radius: u32,
}

struct Rectangle {
    width: u32,
    height: u32,
}

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing circle with radius {}", self.radius);
    }
}

impl Draw for Rectangle {
    fn draw(&self) {
        println!("Drawing rectangle {}x{}", self.width, self.height);
    }
}

fn render_component(component: &dyn Draw) {
    component.draw();
}

fn main() {
    let circle = Circle { radius: 5 };
    let rectangle = Rectangle { width: 10, height: 20 };
    
    render_component(&circle);
    render_component(&rectangle);
}

Slices

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let slice: &[i32] = &arr[..]; // This is a DST
    
    println!("Slice length: {}", slice.len());
    for item in slice {
        println!("Item: {}", item);
    }
}

Type Conversion

Using Into and From Traits

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("{:?}", num);
    
    let int = 5;
    let num: Number = int.into();
    println!("{:?}", num);
}

TryFrom and TryInto

use std::convert::TryFrom;
use std::convert::TryInto;

#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();
    
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

fn main() {
    assert_eq!(EvenNumber::try_from(4), Ok(EvenNumber(4)));
    assert_eq!(EvenNumber::try_from(5), Err(()));
    
    let result: Result<EvenNumber, ()> = 8i32.try_into();
    assert_eq!(result, Ok(EvenNumber(8)));
    
    let result: Result<EvenNumber, ()> = 7i32.try_into();
    assert_eq!(result, Err(()));
}

Practical Examples

Example 1: Custom Error Type with Never Type

use std::fmt;

#[derive(Debug)]
struct Error;

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An error occurred")
    }
}

impl std::error::Error for Error {}

fn might_panic() -> ! {
    panic!("This function never returns!");
}

fn handle_error<T>(result: Result<T, Error>) -> T {
    match result {
        Ok(value) => value,
        Err(_) => might_panic(), // Never returns, so this is safe
    }
}

fn main() {
    let success: Result<i32, Error> = Ok(42);
    let value = handle_error(success);
    println!("Value: {}", value);
    
    // This would panic:
    // let failure: Result<i32, Error> = Err(Error);
    // let value = handle_error(failure);
}

Example 2: Advanced Type System for Configuration

use std::collections::HashMap;
use std::convert::TryFrom;

// Type aliases for clarity
type ConfigKey = String;
type ConfigValue = String;
type Configuration = HashMap<ConfigKey, ConfigValue>;

// Never type for error handling
fn config_error(msg: &str) -> ! {
    eprintln!("Configuration Error: {}", msg);
    std::process::exit(1);
}

// Custom types with conversions
#[derive(Debug)]
struct Port(u16);

impl TryFrom<u16> for Port {
    type Error = &'static str;
    
    fn try_from(value: u16) -> Result<Self, Self::Error> {
        if value > 0 && value < 65536 {
            Ok(Port(value))
        } else {
            Err("Port must be between 1 and 65535")
        }
    }
}

#[derive(Debug)]
struct DatabaseUrl(String);

impl TryFrom<String> for DatabaseUrl {
    type Error = &'static str;
    
    fn try_from(value: String) -> Result<Self, Self::Error> {
        if value.starts_with("postgresql://") || value.starts_with("mysql://") {
            Ok(DatabaseUrl(value))
        } else {
            Err("Database URL must start with postgresql:// or mysql://")
        }
    }
}

fn get_config_value(config: &Configuration, key: &str) -> &str {
    config.get(key).map(|s| s.as_str()).unwrap_or_else(|| {
        config_error(&format!("Missing required configuration key: {}", key))
    })
}

fn main() {
    let mut config = Configuration::new();
    config.insert("database_url".to_string(), "postgresql://localhost:5432/mydb".to_string());
    config.insert("port".to_string(), "8080".to_string());
    
    let db_url_str = get_config_value(&config, "database_url");
    let db_url = DatabaseUrl::try_from(db_url_str.to_string()).unwrap_or_else(|e| {
        config_error(e)
    });
    
    let port_str = get_config_value(&config, "port");
    let port_num: u16 = port_str.parse().unwrap_or_else(|_| {
        config_error("Port must be a valid number")
    });
    
    let port = Port::try_from(port_num).unwrap_or_else(|e| {
        config_error(e)
    });
    
    println!("Database URL: {:?}", db_url);
    println!("Port: {:?}", port);
}

Common Mistakes

โŒ Forgetting Sized Requirement

// This won't compile
fn generic<T>(t: T) {
    // ...
}

// Structs also require sized types by default
struct MyStruct<T> {
    value: T, // Error if T is unsized
}

โœ… Properly Handling Unsized Types

// Use references or smart pointers for unsized types
fn generic<T: ?Sized>(t: &T) {
    // ...
}

// Use Box for storing unsized types
struct MyStruct<T: ?Sized> {
    value: Box<T>,
}

โŒ Incorrect Type Conversion Implementation

struct MyType(i32);

// This implementation is incomplete
impl From<i32> for MyType {
    fn from(item: i32) -> Self {
        // Missing validation or transformation
        MyType(item)
    }
}

โœ… Proper Type Conversion with Validation

#[derive(Debug)]
struct PositiveNumber(u32);

impl TryFrom<i32> for PositiveNumber {
    type Error = &'static str;
    
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value > 0 {
            Ok(PositiveNumber(value as u32))
        } else {
            Err("Value must be positive")
        }
    }
}

Key Takeaways

  • โœ… Type aliases create synonyms for existing types without introducing new types
  • โœ… The never type ! represents values that never return
  • โœ… Dynamically sized types (DSTs) like str and [T] don't have known size at compile time
  • โœ… Use references &str or smart pointers Box<str> to work with DSTs
  • โœ… Trait objects &dyn Trait are a common use of DSTs
  • โœ… From and Into traits provide type conversion
  • โœ… TryFrom and TryInto traits provide fallible type conversion
  • โœ… Follow Rust naming conventions and idioms for advanced types

Ready for Chapter 21? โ†’ Advanced Functions and Closures

๐Ÿฆ€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS โ€ข Open Source