Featured resource
Tech Upskilling Playbook 2025
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Learn more
  • Labs icon Lab
  • Core Tech
Labs

Guided: Rust's Advanced Type System

Enhance your Rust skills by diving into its advanced type system features—trait objects, associated types, and higher-rank trait bounds. In this hands-on lab, you’ll build a flexible plug-in architecture that showcases these features. This lab is designed to help Rust developers write safer, more flexible code by leveraging Rust’s type system to its full potential.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 30m
Last updated
Clock icon Jul 31, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Overview

    This Code Lab explores advanced features of the Rust type system, including:

    • Associated types for abstracting implicit type definitions for trait implementations
    • Trait objects for dynamic polymorphism (via dispatch) and object-safety
    • Higher-ranked Trait Bounds (HRTBs) for working with generic function signatures with closures

    Scenario

    For this lab, you will start with a minimally bootstrapped Rust project built with Cargo. Your task is to design an extensible plugin framework in which each plugin is a modular component with its own behavior, accepts different types of input, and can even call back into your system via closures for additional functionality.


    Additional Information

    There is a solution directory which you can reference at any time to check your implementation. You can run the project as you progress with the cargo run command within the Terminal.

  2. Challenge

    Understanding Associated Types and Trait Objects

    Before you define your Plugin trait, it's crucial to understand two of the key Rust features that underpin this exercise: associated types and trait objects.


    Associated Types

    Associated types are a way for traits to declare placeholder types that implementors can later specify. Instead of requiring a trait to be generic over multiple types (T, U, etc.), you declare associated types inside the trait definition itself. This keeps trait signatures cleaner and shifts type specification to the implementation.

    // generic
    trait Example<T, U> {}
    
    // function must also be generic over T, U
    fn example<T, U, E: Example<T, U>>(){}
    
    // associated types
    trait Example {
      type T
      type U
    }
    
    fn example<E: Example>(){}
    

    Trait Objects

    Trait objects enable runtime polymorphism using dynamic dispatch, letting you write code that can operate on multiple types through a shared trait. This is in contrast to static dispatch, where all type information is resolved at compile time.

    trait Greeter {
        fn greet(&self);
    }
    
    // static dispatch. Compile-time resolution. Faster and allows inlining, but each monomorphized version increases binary size.
    fn greet_static<T: Greeter>(g: T) {
        g.greet(); // compiled directly to the correct method
    }
    
    // dynamic dispatch. Resolved at runtime via vtable. More flexible, but incurs a runtime cost and prevents some optimizations. Utilizes `dyn` keyword.
    fn greet_dynamic(g: &dyn Greeter) {
        g.greet(); // resolved via vtable
    }
    

    Object Safety

    Not all traits can be turned into trait objects. A trait object must be object-safe, meaning it:

    • Must not have any associated types with generics
    • Methods cannot be generic nor return Self
    • Any methods that defy the point above must be made explicitly non-dispatchable (defined with a bound so that it cannot be used with dynamic dispatch)
  3. Challenge

    Implementing Plugins

    At the core of this exercise is the Plugin trait, which provides a unified interface for all plugins in the system. Each plugin defines its own input and output types via associated types, and implements two methods:

    • name(&self) -> &str: Identifies the plugin
    • execute(&self, input: Self::Input) -> Self::Output: Defines the plugin's primary behavior

    This enables flexible plugin behavior with strong type guarantees, without requiring generic parameters at each use site.

    Also note that although the execute method uses Self::Output (an associated type), this does not violate object safety. Self::Output refers to a concrete output type known at the time the trait is used, not a generic type parameter. This is not to be mistaken with returning Self, which would violate object-safety.


    Implementing Plugin

    Head over to src/plugin.rs and implement the Plugin trait.

    Plugin Instructions 1. Define a `trait` called `Plugin` and mark it as public with `pub`. 2. Give it two associated types called `Input` and `Output`. 3. Define the `name` and `execute` function signatures as specified in the previous section.

    Implementing LoggerPlugin

    Now head to src/plugins/logger.rs and implement the Plugin trait for LoggerPlugin.

    LoggerPlugin Instructions 1. Import `Plugin` with `use crate::plugin::Plugin;`. 2. Define an `impl Plugin` block. 3. Define `Input` as a `String` and `Output` as `()`. 4. Return `"LoggerPlugin"` within the `name` function and `println!("[{}] {}", "Logger Output", input);` within the `execute` function.

    Implement MathPlugin

    Now head to src/plugins/math.rs and implement the Plugin trait for MathPlugin.

    MathPlugin Instructions 1. Import `Plugin` with `use crate::plugin::Plugin;`. 2. After the `impl MathPlugin` block, define an `impl Plugin` block. 3. Define `Input` as a tuple of 2 `i32`s and `Output` as an `i32`. 4. Return `"MathPlugin"` within the `name` function. 5. Define a 2 value tuple set to the `input` parameter and do a `match` statement for `self.operation`. 6. If `self.operation` is an add operation, return the sum of the values. If it's a multiply operation, return the product of the values.

    Enabling Plugins in main

    Now head to src/main.rs and enable your plugins to be used in the main method to display their functionality.

    Instructions 1. Import `Plugin` with `use plugin::{Plugin}` at the top. 2. Uncomment all the `println!` statements underneath the `LoggerPlugin` and`MathPlugin` comment sections. 3. Before the `println!` under the `LoggerPlugin` section, define a `logger_obj` variable typed as `Box>`. Set its value to `Box::new(LoggerPlugin::new(LogLevel::Info))`. 4. After the logger `println!`, call `execute()` on `logger_obj` and pass in `"lorem ipsum via trait object\n".to_string()`, or any other `String` of your choosing. 5. In the `MathPlugin` section, create a `sum` variable set to the value of `adder.execute()` and pass in a tuple with values 5 and 7. This variable should be defined between the two `println!` statements. 6. Do the same as step 5 for the multiplier instance of `MathPlugin`.

    After enabling these plugins, run the program with cargo run and you should see the following spoiler in your Terminal.

    Spoiler
    Running plugin (trait object): LoggerPlugin
    [Logger Output] lorem ipsum via trait object
    
    Running plugin: MathPlugin
    Sum result: 12
    
    Running plugin: MathPlugin
    Product result: 35
    
  4. Challenge

    Higher-Rank Trait Bounds

    In this step, you will introduce higher-ranked trait bounds (HRTBs) into your plugin framework. HRTBs allow you to write function signatures that accept closures (or other generic types) that must be valid for all possible lifetimes—a crucial feature when working with borrowed data whose lifetime cannot be known in advance.

    Consider a plugin that wants to call a user-provided callback with an internal string like its name or description. The plugin holds this string internally, meaning the callback must accept a borrowed string. The callback must then assume a specific lifetime, but you don’t necessarily know what that lifetime is—which abstraction and makes borrowing difficult.

    To solve this, you can use the HRTB syntax via for<'a>. This tells the compiler: “This closure must accept a &str of any lifetime.” Now, whether the plugin's internal string is a long-lived reference or a temporary borrow, it safely compiles and works correctly.


    Implementing PluginWithCallback

    Head over to src/plugin.rs and implement the PluginWithCallback trait.

    PluginWithCallback Instructions 1. Define a `trait` called `PluginWithCallback` and mark it as public with `pub`. 2. Define a `with_callback_mut` method with the following method signature: `fn with_callback_mut(&self, callback: F) where F: for<'a> FnMut(&'a str);`

    Implementing EchoPlugin

    Now head to src/plugins/echo.rs and implement the Plugin and PluginWithCallback trait for EchoPlugin.

    EchoPlugin Instructions 1. Import `Plugin` and `PluginWithCallback` using `use crate::plugin::{Plugin, PluginWithCallback};`. 2. Define a `impl Plugin` block and implement the `Plugin` trait similar to the previous plugins. Use `String` for both input and output types, return `"EchoPlugin"` within the `name()`, and return the `input` parameter within `execute()`. 3. Define a `impl PluginWithCallback` block and implement the `with_callback_mut()` method. Within the method body, define a `message` variable set to `format!("{}{}", self.prefix, "callback triggered");`, then do `callback(&message)`.

    Putting it into main

    Now head to src/main.rs and incorporate the EchoPlugin and it's HRTB generic method.

    Instructions 1. Import `PluginWithCallback` with `use plugin::{Plugin, PluginWithCallback}` at the top. 2. Uncomment all the `println!` statements underneath the `EchoPlugin` comment sections. 3. Beneath the `use callback` comment section, call `with_callback_mut` on the `echo` plugin. 4. Pass in the following closure and callback function: `|msg| { println!("Callback received message: {}", msg); }`

    After setting up EchoPlugin, run the program with cargo run and you should see the following spoiler in your Terminal.

    Spoiler
    Running plugin (trait object): LoggerPlugin
    Logger Output] lorem ipsum via trait object
    
    Running plugin: MathPlugin
    Sum result: 12
    
    Running plugin: MathPlugin
    Product result: 35
    
    Running plugin: EchoPlugin
    Echoed output: Hello!
    
    Callback received message: [Echo] callback triggered
    

    Note: The with_callback_mut function that utilizes a HRTB is implemented as a generic method, which means that the PluginWithCallback trait is not object-safe. Any types that implement this trait cannot be a trait object, and attempting to do so would throw an error saying it is dyn-incompatible. This only applies to the example shown in this lab as it is possible to implement an HRTB on a method while keeping the trait object-safe.

  5. Challenge

    Conclusion

    Great job on completing this lab!

    In doing so, you explored advanced type system features in Rust by building a modular plugin framework. You used trait objects with associated types to enable runtime polymorphism and flexible plugin composition. You introduced higher-ranked trait bounds (HRTBs) to support callbacks that accept references with arbitrary lifetimes. Along the way, you examined object safety and contrasted the pros and cons of dynamic vs. static dispatch.

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.