Is there a simple way to overload functions?

I've searched on the forum and in "the book", but it seems that how to overload a function or a method is not mentioned in "the book" and the way given by someone on the forum is a bit complicated and seems to need features of nightly versions of Rust. I'd like to ask whether Rust officially support overloading or what is the Rusty way to handle the situation where we need overloading. If Rust doesn't officially support overloading, why?

1 Like

There's no function overloading in Rust.

Usually you make separate functions foo(), foo_mut(), try_foo(), foo_with_bar().

For some things it's possible to give overload-like interface using traits. For example, to accept any type that is string-like, you can use:

fn foo(s: impl AsRef<str>) {s.as_ref()}
// or
fn foo(s: impl Into<String>) {s.into()}

and you can do more this way by defining your own traits. There are some even cleverer trait hacks to simulate overloading further, but they're so hacky I won't even mention them.

Another option is to use macro_rules to define a macro that chooses function based on number or syntax of the arguments.

foo!(1,2);
foo!("x" => y);
5 Likes

Because function overloading is conceptually a missconception. Logically if we assume that function name is describing a behaviour, there is no reason, that the same behaviour may be performed on two completelly independent, not simillar types - which is what overloading actually is. If there is a situation, where operation may be performed on set of types, its clear, that they have something in common, so this commoness shouold be extracted to trait, and then generalizing function over trait is straight way to go.

I'm just curios about your sentence:

Could you point such a situation?

11 Likes

The "why" is because type inference and overloading are incompatible. (modern C++ may appear to have type inference, but it is nothing close to Rust's, and never will be).

Traits are how we can have the best of both worlds.

14 Likes

What you said makes sense. I used to take overloading for granted and haven't thought about it seriously. Now it seems that overloading is less important.
It actually took me a while to figure out an example. Maybe functions with defaults? Like the constructor of InputStreamReader in Java, we can just pass an input stream(presuming UTF8 encoding by default) to it or moreover specify the encoding of the input stream, so we need a constructor with only one parameter and another with two parameters.

But a limited version of overloading may not be so incompatible with type inference, say, we only permit function overloading based on the number of parameters, and it seems type inference still work.

1 Like

Comparing to other languages API, especially Java isn't best idea - Rust type system doesn't fit to this kind of OOP (which doesn't mean, that Rust declines OOP in general).

Functions with defaults is in general wider problem. Is it good to hide that functions takes any additional attirbutes? I actually prefer that for optional attibutes I have to write Default::default() (or None - depending on API). And constructors are also very specific example - first of all Rust doesn't have constructors like Java, they are just custom static function (which are typically called new, but there is no problem to create two functions: new, and with_encoding - this is common pattern in Rust). Also Builder pattern works very well with Rust and is commonly using in place of compex constructors.

6 Likes

Get! And THANKS! Although I think it is a bit verbose to write two constructor functions like that (got spoiled by Java), it just a minor problem and it may be helpful to remind API users to notice defaults.

1 Like

I thought about this in the past, and I recall there being a tricky case with functions that are used as path expressions.

// where `fn foo<T>` exists
let f = foo; // or equivalently, foo::<_>

Currently, the type of a f would be a ZST representing fn foo with a single unknown generic type argument. Notice that the type parameters are part of the type rather than part of the Fn{Mut,Once} impls; that's what makes turbofish syntax possible.

Notice how the arity is not known yet because we didn't call it. So I think even overloading on arity will lead to complications, but I am not sure.


I definitely think this is possible. As long as only a single function signature appears in the source code, I don't think there is any ambiguity.

The bigger issue here would be convincing other Rustaceans that the feature is worthy of inclusion! I am divided on the issue; I think I've seen some cases where it would be nice, but the feature doesn't offer much power (e.g. if you have two default arguments, you can't skip one unless the language also has keyword arguments), and using the Builder pattern offers more possibilities for future API evolution.

Verbosity isn't bad. For implementator its just like adding one line:

fn new() -> Self { Self::with_encoding(some_default) }

For calee however it is very big readablility upgrade:

MyStream::new(); // Stream with very much everything default
MyStream::with_encoding(CP1047); // I clearly know what is an argument

And probably you may say, that encoding should be some enum, and it would be prefixed anyway, like Encoding::CP1047, but its possible, that:

  • an argument type is more generic - let say int, or string
  • an argument is produced by some function

This additional verbosity makes code much easier to read and reason, without checking documentation every single line.

2 Likes

One thing languages like Java or C# often use overloaded functions for is to mimic ad-hoc polymorphism. Basically this just means that sometimes you have a set of unrelated types that you want to all apply some operation to. For example, PrintStream in Java has

void println(boolean);
void println(char);
void println(int);
void println(String);

Conceptually, these all do the same thing even if the implementation details differ slightly. In Rust, you'd use traits to model this:

trait Printable {
  fn print(&self, writer: &io::Write);
}

impl Printable for bool {
  fn print(&self, writer: &io::Write) { ... }
}

impl Printable for char {
  fn print(&self, writer: &io::Write) { ... }
}

impl Printable for i32 {
  fn print(&self, writer: &io::Write) { ... }
}

//etc

As an added benefit, you can implement this for your own types while you can't add overloads to class you don't control in Java/C#.

19 Likes

I agree that overloading by types will get in the way of type inference (and C++-style overloading with implicit type conversion hierarchies is very tricky already).

That's because you model it as a single function that has multiple overloads. How about modelling it as multiple functions with the same name instead? (and the name is disambiguated by number of args).

So instead of a single vague type, you'd have concrete type, and would need to specify it:

let f = foo::<I want the version that has 2 arguments>;
f(1,2);
// f(1); // error

That is indeed an option, it's just up to an incredible amount of bikeshedding. Were the language designed with this in mind from the getgo, we probably could've had something akin to Erlang's "slash notation" where the arity is basically considered to be part of the name. (though perhaps we wouldn't use the slash character for this)

3 Likes

And to add onto @wesleywiser's post, they frequently do that to avoid boxing an object unnecessarily (Which Object would do in C#), but in rust, nothing is implicitly boxed, so you always know what is happening.

Overloading is, as mentioned earlier, done with traits, and you mention the "nightly" way to do this (I'm the author of the overloadable crate) but in fact that's just repeated trait implementation on the same type. It's akin to doing this (Without the Fn trait-specific stuff):

//Dummy struct
struct function_name;
//Trait to represent the Fn traits
trait Callable<Args> {
    type Output;
    fn call(&self, args: Args) -> Self::Output;
}
//Called with nothing
impl Callable<()> for function_name { /**/ }
//With one number
impl Callable<usize> for function_name { /**/ }
//With a string 
impl Callable<&str> for function_name { /**/ }
//With a generic and a number
impl<T: Debug> Callable<(T, usize)> for function_name { /**/ }

Therefore I can do this:

let x = function_name(2usize);
//Is actually:
let __func = function_name; //The zero-sized struct
let x = __func.call(2usize);
2 Likes

if it is just for convenience, I think it can be safely taken as an alias of the function foo like what #define in C dose, but as I haven't got familiar with Rust and not known how Rust actually realize it, I'm not sure whether taking it as an alias is appropriate.

Thanks for the clear explanation and sorry for having not referred to your work. That explanation helps a lot because I'm just a beginner of Rust. If I haven't got that wrong, overloaded "functions" with different arities should be realized in a similar way using macros in the overloadable crate.

there is no reason, that the same behaviour may be performed on two completelly independent, not simillar types

Well, it's very hard to convert APIs designed for other languages into Rust with this. For example, there's the tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_array_buffer_view function in wasm-bindgen that's totally easy to remember. In JavaScript, it's simply called texSubImage2d.

Yes, the arity of the functions must all be different, which is enforced by the trait system, where we can't say the following:

trait Foo<T> {/**/}
impl Foo<usize> for () {/**/}
impl Foo<usize> for () {/**/}

because it makes no sense to implement the same trait on the same object more than once.
This is agnostic of the return type because it's in an associated type, which is not part of the name of the trait you're implementing, and is instead part of the implementation (Mimicking overloadable functions elsewhere):

public class Example {
    public static double Foo1() {/**/}
    public static void Foo1() {/**/} //Error function arity is independent of return type
}

On a separate note, you mentioned macros (Which overloadable uses to generate Fn impls) which themselves can act similar to overloadable functions in that multiple inputs can match the same name:

macro_rules! foo {
    (option one) => { /**/ };
    (option two) => { /**/ };
}

Which also support variadic-style parameters (Preferably called repetitions):

macro_rules! foo {
    ($(repeated and separated by commas),+) => { /**/ };
}
3 Likes

I think what macros do is actually meta-programming which is not related to overloading. I don't know what Rust treats functions and methods as, but if it takes them as instances of a special struct, similar to that Java can take them as instances of Method , I think what you did in overloadable is fine to be taken as a syntax sugar to realize restricted overloading.

I think what @OptimisticPeach means is that a rust macro is the closest thing rust has that looks like overloading in other languages.

let v_nums = vec![1,2,3]
let v_str = vec!["1", "2", "3"]

You can get pretty close with trait bounded generics. When/if specialization stabilizes then you have overload by trait type.

#![feature(specialization)]
trait SomeTrait {
    fn foo(&self);
}
impl<T> SomeTrait for T {
    default fn foo(&self) {
        // do default thing
    }
}
impl<T : Debug> SomeTrait for T {
    fn foo(&self) {
        // do something else if T is Debug
    }
}
2 Likes