Rust: trying function overloading

Original author: Casper
  • Transfer
  • Tutorial

Rust has no function overload: you cannot define two functions that have the same name. The compiler will display a message that you have a double task of the same definition, even if they contained different types of arguments.


After several attempts, the problem was successfully solved. Like - under the cut.


Games with traits do not work.


trait FooA { fn foo(_: i32); }
trait FooB { fn foo(_: &str); }

struct Foo;
impl FooA for Foo { fn foo(_: i32) { println!("FooA"); } }
impl FooB for Foo { fn foo(_: &str) { println!("FooB"); } }

Let's try to call a function with a type argument &str .


fn main() {
    Foo::foo("hello");
}

This does not compile, because the call is ambiguous and Rust does not try to figure out which of their functions, depending on the types / number of arguments, is called. If we run this code, the compiler will report that there are several functions that can be called in this case.


On the contrary, this example requires an unambiguous indication of the function being called:


fn main() {
    <Foo as FooB>::foo("hello");
}

Code


However, this negates all the benefits of overloading. At the end of this article, I will show that on Rust, a traditional function overload is implemented - through the use of traits and generalized programming - generics.


Static polymorphism


To allow the method to accept various types of arguments, Rust uses static polymorphism with generics.


The generalized parameter is limited by the type: the function accepts only arguments of types that implement the required types. The type imposes restrictions on the set of actions that you can do with respect to the argument.


They can be simple, for example, AsRef to allow your API to accept more options for arguments:


fn print_bytes<T: AsRef<[u8]>>(bytes: T) {
    println!("{:?}", bytes.as_ref());
}

In the calling code, this looks like an overload:


fn main() {
    print_bytes("hello world");
    print_bytes(&[12, 42, 39, 15, 91]);
}

Code


Probably the best example of this is receiving several types of arguments
type ToString :


fn print_str<T: ToString>(value: T) {
    let s = value.to_string();
    println!("{}", s);
}

fn main() {
    print_str(42);
    print_str(3.141593);
    print_str("hello");
    print_str(true);
    print_str('');
}

Code


This kind of overload makes your API more usable for your users. They will not need to burden themselves with the conversion of arguments to the desired type, the API does not require this. The result is an API that is nice to work with.


This approach has advantages over the usual overload, because the implementation of types of (user) types allows your API to accept different user types.


Habitual overloading offers much more flexibility in implementation and in the number of arguments accepted in overloaded functions. The latter problem can be solved by using tuples as the container for the set of arguments, but this is not very attractive. An example of this is trait ToSocketAddrs in the standard library.


Sideshow: Redundant Generic Code


Beware of clogging with redundant generic code. If you have a generalized function with a lot of non-trivial code, specialized copies of functions are created for each call to this function with arguments of different types. This happens even if you translate the input arguments into variables of the required types at the beginning of the function.


Fortunately, there is a simple solution to the problem: implementing a private function without generics that accepts the types you want to work with. While public functions perform type conversions and pass the execution of your private function:


mod stats {
    pub fn stddev<T: ?Sized + AsRef<[f64]>>(values: &T) -> f64 {
        stddev_impl(values.as_ref())
    }
    fn stddev_impl(values: &[f64]) -> f64 {
        let len = values.len() as f64;
        let sum: f64 = values.iter().cloned().sum();
        let mean = sum / len;
        let var = values.iter().fold(0f64, |acc, &x| acc + (x - mean) * (x - mean)) / len;
        var.sqrt()
    }
}
pub use stats::stddev;

Despite the fact that the function is called with two different types ( &[f64] and &Vec<f64> ), the main logic of the function is implemented (and compiled) only once, which prevents excessive bloating of the binaries.


fn main() {
    let a = stddev(&[600.0, 470.0, 170.0, 430.0, 300.0]);
    let b = stddev(&vec![600.0, 470.0, 170.0, 430.0, 300.0]);

    assert_eq!(a, b);
}

Code


Checking the boundaries


Not every overload falls into this category of simple argument conversions. Sometimes you really need different logic to handle different sets of accepted arguments. For these cases, you can define your type to implement the program logic of your function:


pub struct Foo(bool);

pub trait CustomFoo {
    fn custom_foo(self, this: &Foo);
}

This makes the type very awkward, because the self arguments are reversed:


impl CustomFoo for i32 {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) i32: {}", this.0, self);
    }
}
impl CustomFoo for char {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) char: {}", this.0, self);
    }
}
impl<'a, S: AsRef<str> + ?sized> CustomFoo for &'a S {
    fn custom_foo(self, this: &Foo) {
        println!("Foo({}) str: {}", this.0, self.as_ref());
    }
}

The type cannot be hidden as an implementation detail. If you choose to type private, the compiler will generate the following: private trait in public interface .


Let's wrap the trait:


pub struct Foo(bool);

impl Foo {
    pub fn foo<T: CustomFoo>(&self, arg: T) {
        arg.custom_foo(self);
    }
}

fn main() {
    Foo(false).foo(13);
    Foo(true).foo(''));
    Foo(true).foo("baz");
}

Code


Application of this technique can be found in the standard library in type Pattern , which is used by various functions that are or are seeking in any way correlate string, for example str::find .


Unlike you, the standard library has the ability to hide these types, while at the same time giving them the opportunity to be used in public interfaces through an attribute #[unstable] .


Get one shot at two birds with one stone


There is a better way that will give us almost all the possibilities of generally accepted function overloading.


Create a trait for the function whose signature you want to overload with the generalized parameters in place of the "overloaded" parameters.


trait OverloadedFoo<T, U> {
    fn overloaded_foo(&self, tee: T, yu: U);
}

Type restrictions in Rust are a very powerful tool.


When implementing a method, simply limit Self it to implement the type and the general parameters that your type needs. For Rust, this is enough:


struct Foo;
impl Foo {
    fn foo<T, U>(&self, tee: T, yu: U) where Self: OverloadedFoo<T, U> {
        self.overloaded_foo(tee, yu)
    }
}

After that, implement the trait for all types for which you want to provide overloading:


impl OverloadedFoo<i32, f32> for Foo {
    fn overloaded_foo(&self, tee: i32, yu: f32) {
        println!("foo<i32, f32>(tee: {}, yu: {})", tee, yu);
    }
}

They can be empty implementing blocks. Make sure that the types are consistent with each other. Compiler messages here are very helpful.


impl<'a, S: AsRef<str> + ?Sized> OverloadedFoo<&'a S, char> for Foo {
    fn overloaded_foo(&self, tee: &'a S, yu: char) {
        println!("foo<&str, char>(tee: {}, yu: {})", tee.as_ref(), yu);
    }
}

It's all!


Try removing the comment from the last line and look at the error message when the function is called with arguments for which there is no corresponding signature.


fn main() {
    Foo.foo(42, 3.14159);
    Foo.foo("hello", '');
    // Foo.foo('', 13); // ограничения типажа не соблюдены
}

Code


Output


As always, the way you choose to get the effect of overloading functions depends on your needs. I set myself the goal of examining several overload emulation techniques and their limitations so that you can make the right decision on how to use it in your code.

Only registered users can participate in the survey. Please come in.

How do you feel about function overloading in Rust?

  • 46.5% Should add 33
  • 18.3% No need, for complicate the language 13
  • 35.2% Not necessary, for there is enough 25