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 task 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("hello");
}

The 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 - generic'ov.


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, AsRefto allow your API to accept more options for arguments:


fn print_bytes>(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]);
}

The code


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


fn print_str(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('');
}

The code


This kind of overload makes your API more user friendly. 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 traitToSocketAddrs in the standard library.


Sideshow: Redundant Generic Code


Beware of clogging with redundant generic code. If you have a generic function with a lot of nontrivial code, then 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>(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 ), the main logic of the function is implemented (and compiled) only once, which prevents excessive bloating of the binaries.&Vec


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);
}

The 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 selfarguments 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 + ?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(&self, arg: T) {
        arg.custom_foo(self);
    }
}
fn main() {
    Foo(false).foo(13);
    Foo(true).foo(''));
    Foo(true).foo("baz");
}

The code


Application of this technique can be found in the standard library in typePattern , 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 {
    fn overloaded_foo(&self, tee: T, yu: U);
}

Type restrictions in Rust are a very powerful tool.


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


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

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


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

Они могут быть пустыми реализующими блоками. Следите за тем, чтобы типажи были согласованы между собой. Сообщения компилятора здесь очень помогают.


impl<'a, S: AsRef + ?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);
    }
}

Это все!


Попробуйте удалить комментарий с последней линии и посмотрите на сообщение об ошибке, когда функция вызывается с аргументами, для которых нет соответствующей сигнатуры.


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

Код


Вывод


Как и всегда, способ, который вы выберете для получения эффекта перегрузки функций, зависит от ваших потребностей. Я ставил перед собой цель рассмотрения нескольких техник эмуляции перегрузки и их ограничения, чтобы вы могли принять правильное решение по ее использованию в своем коде.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вы относитесь к перегрузке функций в Rust?

  • 43.9%Следует добавить29
  • 19.6% Not necessary, for it will complicate the language 13
  • 36.3% Not necessary, for there are enough that there are 24

Also popular now: