Rust does not have function overloading: you cannot define two functions that have the same name. The compiler will display a message that you have a double assignment of the same definition, even if they contain different types of arguments.
After several attempts, the problem was successfully solved. How - under the cut.
Games with types 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 an argument of type &str
.
fn main() { Foo::foo("hello"); }
It 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 called function:
fn main() { <Foo as FooB>::foo("hello"); }
However, this negates all the benefits of overloading. At the end of this note, I will show that Rust implements the traditional overloading of functions - through the use of types and generic programming - generics.
To allow the method to accept various types of arguments, Rust uses static polymorphism with generics.
The generic parameter is limited by type: the function accepts only arguments of such types that implement the required types. Description imposes restrictions on a set of actions that you can do in relation to an argument.
They can be as simple as AsRef
, for example, to allow your API to take more options for arguments:
fn print_bytes<T: AsRef<[u8]>>(bytes: T) { println!("{:?}", bytes.as_ref()); }
In the calling code, this looks like overloading:
fn main() { print_bytes("hello world"); print_bytes(&[12, 42, 39, 15, 91]); }
Probably the best example of this is taking 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(''); }
This kind of overload makes your API more convenient for your users to use. They will not need to burden themselves with translating arguments into the necessary type, the API does not require this. The result is an API that is a pleasure to work with.
This approach has advantages in comparison with the usual overload, because the implementation of types (user) types allows your API to accept different user types.
The familiar overload offers much more flexibility in the implementation and in the number of arguments taken in the overloaded functions. The latter problem can be solved by using tuples as a container for a set of arguments, but this is not very attractive. An example of this is the type ToSocketAddrs
in the standard library.
Be wary of clogging with excessive generic code. If you have a generic function with a large number of non-trivial code, then for each call of this function with arguments of different types, specialized copies of the functions are created. This happens even if every time at the beginning of the function you translate input arguments into variables of the desired types.
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 do type conversions and pass on 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;
Although 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 swelling 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); }
Not every overload falls into this category of simple argument conversions. Sometimes you really need different logic to handle different sets of arguments taken. For these cases, you can define your own 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 clumsy, for self
and arguments are swapped:
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()); } }
Description cannot be hidden as an implementation detail. If you decide to make the type private, the compiler will output the following: private trait in public interface
.
Let's make a wrapper over the type:
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"); }
The application of this technique can be found in the standard library in the Pattern
, which is used by various functions that search for or in some way match strings, for example, str::find
.
Unlike you, the standard library has the ability to hide these types, while at the same time allowing them to be used in public interfaces by means of the #[unstable]
attribute.
There is a better way that will give us almost all the features of common function overload.
Create a type for the function whose signature you want to reload with generalized parameters in the place of the "overloaded" parameters.
trait OverloadedFoo<T, U> { fn overloaded_foo(&self, tee: T, yu: U); }
Restrictions on types in Rust are very powerful tools.
When implementing the method, simply restrict Self
to implement the type and generalized 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 type for all types for which you want to provide an overload:
impl OverloadedFoo<i32, f32> for Foo { fn overloaded_foo(&self, tee: i32, yu: f32) { println!("foo<i32, f32>(tee: {}, yu: {})", tee, yu); } }
They may be empty implementing blocks. Make sure that the types are consistent with each other. Compiler messages are very helpful here.
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); // }
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 techniques for emulating overload and limiting them so that you can make the right decision on its use in your code.
Source: https://habr.com/ru/post/351570/
All Articles