👀 Watch Rust 🦀 live coding videos on our YouTube Channel.

Why use polymorphism in Rust? #

When it comes to polymorphism in Rust, which means that you want to be intentionally “vague” about what arguments a function can receive or what values it can return, there are roughly two approaches: static dispatch and dynamic dispatch. They are both tightly related to the notion of sidedness in Rust.

There are many legitimate reasons to be intentionally vague about the types of arguments a function can receive or the values it can return. Here are a few:

  • Testing: You want swap out the implementation of a function with a test mock or test fixture, so that you can test the function in isolation.
  • Extensibility: You want to accommodate integrations with other code that you don’t control, and you want to be able to use dependency injection to provide the intended behaviors (from) systems that you don’t control.
  • Reuse: You want to reuse the same code in multiple places, since they only operate on on aspect (or trait) of the data.

Here are the two approaches to polymorphism in Rust:

static dynamic
receive receive
return return

There are pros and cons to each approach:

approach pros cons
static Compile time checks and dispatch. No runtime overhead. Code is more difficult to read and write since generics and their often verbose trait bounds have to be spread to the caller.
dynamic Code is more concise and easier to read and write since the trait objects are localized to the function that accepts or returns them. Runtime overhead due to dynamic dispatch. Vtable lookup is required due to type erasure.

Here are some helpful links to learn more about this topic:

Video of this in action in the real world #

This blog post only has a short example to illustrate both approaches to polymorphism in Rust. To see how these ideas can be used in production code, with real-world examples, please watch the following video on the developerlife.com YouTube channel.

Short example to illustrate both approaches #

The code for this example lives here.

Let’s look a single example (that fits in one file) that illustrates both approaches to polymorphism in Rust. You can run cargo new --lib dyn-dispatch to create a new library crate, and then run cargo add rand. Then you can add the following code to the src/lib.rs file.

This first part is the setup for this example. We have two structs, each of which implements the Error trait. We want to be able to use both structs in functions that can receive or return Error trait objects.

use std::error::Error;
use std::fmt::Display;

// ErrorOne.
mod error_one {
    use super::*;

    #[derive(Debug)]
    pub struct ErrorOne;

    impl Display for ErrorOne {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "ErrorOne")
        }
    }

    impl Error for ErrorOne {}
}
use error_one::ErrorOne;

// ErrorTwo.
mod error_two {
    use super::*;

    #[derive(Debug)]
    pub struct ErrorTwo;

    impl Display for ErrorTwo {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "ErrorTwo")
        }
    }

    impl Error for ErrorTwo {}
}
use error_two::ErrorTwo;

In some of the code we will need to make a random decision, so we’ll use the rand crate to generate random booleans.

// Random boolean generator.
pub fn random_bool() -> bool {
    rand::random()
}

Here’s the code for the static dispatch approach, using generics, trait bounds, and compiler monomorphisation.

// Static dispatch.
mod static_dispatch {
    use super::*;

    mod receives {
        use super::*;

        pub fn accept_error<E: Error>(error: E) {
            println!("Handling ErrorOne Debug: {:?}", error);
            println!("Handling ErrorOne Display: {}", error);
        }

        pub fn accept_error_with_syntactic_sugar(error: impl Error) {
            println!("Handling ErrorOne Debug: {:?}", error);
            println!("Handling ErrorOne Display: {}", error);
        }
    }

    mod returns {
        use super::*;

        pub fn return_error_one() -> ErrorOne {
            ErrorOne
        }

        pub fn return_error_two() -> ErrorTwo {
            ErrorTwo
        }

        // 🚨 DOES NOT WORK! Need dynamic dispatch.
        // pub fn return_single_error() -> impl Error {
        //     if random_bool() {
        //         ErrorOne
        //     } else {
        //         ErrorTwo
        //     }
        // }

        pub fn return_single_error() -> impl Error {
            return ErrorOne;
        }
    }
}

Finally, here’s the code for the dynamic dispatch approach, using trait objects and vtables to enable runtime polymorphism.

// Dynamic dispatch.
mod dynamic_dispatch {
    use super::*;

    mod receives {
        use super::*;

        pub fn recieve_error_by_ref(error: &dyn Error) {
            println!("Handling Error Debug: {:?}", error);
            println!("Handling Error Display: {}", error);
        }

        pub fn example_1() {
            let error_one = ErrorOne;
            recieve_error_by_ref(&error_one);
            let error_two = ErrorTwo;
            recieve_error_by_ref(&error_two);
        }

        pub fn receive_error_by_box(error: Box<dyn Error>) {
            println!("Handling Error Debug: {:?}", error);
            println!("Handling Error Display: {}", error);
        }

        pub fn example_2() {
            let error_one = ErrorOne;
            let it = Box::new(error_one);
            receive_error_by_box(it);
            let error_two = ErrorTwo;
            receive_error_by_box(Box::new(error_two));
        }

        pub fn receive_slice_of_errors(arg: &[&dyn Error]) {
            for error in arg {
                println!("Handling Error Debug: {:?}", error);
                println!("Handling Error Display: {}", error);
            }
        }
    }

    mod returns {
        use super::*;

        pub fn return_one_of_two_errors() -> Box<dyn Error> {
            if random_bool() {
                Box::new(ErrorOne)
            } else {
                Box::new(ErrorTwo)
            }
        }

        pub fn return_one_of_two_errors_with_arc() -> std::sync::Arc<dyn Error> {
            if random_bool() {
                std::sync::Arc::new(ErrorOne)
            } else {
                std::sync::Arc::new(ErrorTwo)
            }
        }

        pub fn return_slice_of_errors() -> Vec<&'static dyn Error> {
            let mut errors: Vec<&dyn Error> = vec![];
            if random_bool() {
                errors.push(&(ErrorOne));
            } else {
                errors.push(&(ErrorTwo));
            }
            errors
        }

        pub fn mut_vec_containing_different_types_of_errors(mut_vec: &mut Vec<&dyn Error>) {
            mut_vec.push(&ErrorOne);
            mut_vec.push(&ErrorTwo);
        }
    }
}

Build with Naz video series on developerlife.com YouTube channel #

You can watch a video series on building this crate with Naz on the developerlife.com YouTube channel.

📦 Install our useful Rust command line apps using cargo install r3bl-cmdr (they are from the r3bl-open-core project):
  • 🐱giti: run interactive git commands with confidence in your terminal
  • 🦜edi: edit Markdown with style in your terminal

giti in action

edi in action

Related Posts