Method Overloading in Rust… Kinda

A compilation-ready source for this article can be found here.

I designed a struct in Rust which contains various bits of file information. Presented here is a stripped down version which holds only the extension or an empty String if no extension is present.

pub struct FileExt {
    extension: String
}

When creating a new FileExt object, I’d like the flexibility of passing the FileExt::from() method a &str, or &String, or &OsStr, or &OsString, or &Path without having to write separate from() methods i.e. from_str(), from_osstr(), and from_path(). The client application will only ever deal with String objects and &str references, but it will need to modify the path of a file for writing purposes. It doesn’t make sense to muck up the client with Path object logic or parsing and modifying path strings.

In C++ and several other languages, the compiler is designed to analyze function names, return types, and the parameters calling these combinatons signatures: int func() has a different signature from int func(int) which has a different signature from int func(&foo_class). This is often accomplished through name mangling and may manifest as:

  • int func() –> int __func_v()
  • int func(int) –> int __func_i(int)
  • int func(foo) –> int __func_foo(&foo_class)

Rust doesn’t allow this blatant of an overloading:

impl FileExt {
    pub fn from(path: &str) -> FileExt {
        // Some implementation
    }

    pub fn from(path: &Path) -> FileExt {
        // Some other implementation
    }

    // Continued method implementations ...

Here is the resulting error:

error[E0201]: duplicate definitions with name `from`:
  --> src\main.rs:16:5
   |
10 | /     pub fn from(path: &str) -> FileExt {
...  |
14 | |     }
   | |_____- previous definition of `from` here
15 |
16 | /     pub fn from(path: &Path) -> FileExt {
...  |
33 | |     }
   | |_____^ duplicate definition

One solution is to separate the methods by signature and name them appropriately. Many such struct methods already exist by default in Rust.

pub fn from_str(path: &str) -> FileExt {
    let path = Path::new(path);

    FileExt::build(&path)
}

pub fn from_path(path: &Path) -> FileExt {
    FileExt::build(&path)
}

fn build(path: &Path) -> FileExt {
    // Some unified implementation
}

This approach is a practical, clear, and acceptable answer. Icarus, however, did not fly so spectacularly close to the sun by being practical, clear, and acceptable. No. We are here for the deepest lore and the blackest programming magic. Other coders must gaze upon our works, and despair!

Recall that we can metaprogram a function that accepts any Type which implements a particular trait or set of traits. This concept is similar to interfaces in C#.

fn showoff<T: Display>(t: T) {
    println!("Here is the value: {}", t);
}

fn main() {
    let number: u32 = 15;
    let name = String::from("Dr. Acula");

    showoff(b);
    showoff(name);
}

The code lets the Rust compiler know that showoff() must be passed any type which implements the rules to satisfy the Display trait. Since both u32 and String implement the trait we require, the output results in a very predictable way:

Here is the value: 15
Here is the value: Dr. Acula

For FileExt to accept a &str, or &OsStr, or &Path in its from() method, the signature needs to look like this:

impl FileExt {
    pub fn from<P: AsRef<Path> + AsRef<OsStr> + ?Sized>(path: &P) -> FileExt {
        // Implementation...

It can also be written as:

impl FileExt {

    pub fn from<P>(path: &P) -> FileExt 
    where
        P: AsRef<Path> + AsRef<OsStr> + ?Sized
    {
         // Implementation...

Whatever Type is passed into the from() method for FileExt, it neds to implement both the AsRef<Path> and AsRef<OsStr> traits. Since these will usually be strings or have references to strings in them, we relax the requirement that the Type’s size be known at compile-time with the ?Sized syntax. The following will now all be valid code:

let my_file_1 = FileExt::from("C:\\foo.txt");
    
let path_2 = String::from("C:\\bar.bat");
let path_3 = Path::new("C:\\foobar.exe");
    
let my_file_2 = FileExt::from(&path_2);
let my_file_3 = FileExt::from(&path_3);