Why Rust doesn't have function overloading?

Such as title. Why Rust doesn't have function overloading?

Some one code:

struct Serial{}

impl Serial{
    fn open(port:&str){}
    fn open(port:&str,baut:u32){}
}

Why?

Rust has a more general mechanism for achieving the same effects (and more) in a more uniform manner: traits and generics.

2 Likes

Do you have any information? I'll study.

Thks, I find official blog Abstraction without overhead: traits in Rust | Rust Blog

1 Like

It doesn't. In this case the idiomatic solution would be a second method like:

fn open_with_baut(port:&str,baut:u32)
1 Like

Yeah, I just be curious.

Rust has type inference, so overloading of types of arguments of methods wouldn't work well. It could lead to surprising types being chosen, or just end up being ambiguous and require user to specify type hints.

There could be overloading based on the number of arguments, since that still allows type inference to look up the right method before knowing the types. In the internals forum you'll find a megathread discussing whether Rust should or should not have this:

8 Likes

Does anyone know if there is an updated version of this blog post? It doesn't mention the dyn keyword for example.

Rust has type inference, so overloading of types of arguments of methods wouldn't work well. It could lead to surprising types being chosen, or just end up being ambiguous and require user to specify type hints.

Yet this particular version of overloading is perfectly supported:

fn foo(x: impl core::fmt::Display) {
    print!("{x}\n")
}

fn main() {
    foo(1);
    foo(4.2);
    foo("Hello");
}

You need to first declare your function as “function with overloaded types” and then implement it separately, but it works.

There could be overloading based on the number of arguments, since that still allows type inference to look up the right method before knowing the types.

And there actually is such overloading. On nightly.

Which clearly shows that the decision not to support overloading is not technical but political. It's actually perfectly implementable and it's actually implemented in the compiler.

It's just not exposed to the users on stable. So… please don't try to justify it on technical grounds. It's just not fair.

3 Likes

It's not fair to accuse compiler developers of having a "political decision" of not stabilizing something they're not ready to stabilize, either.

14 Likes

Despite the access pattern, this isn't overloading.

When a fn is overloaded (i.e. ad-hoc polymorphism is applied to it), it means that there are actually multiple fns with the same name that differ in their signatures. Among other things, this means that each fn in the overload group knows what type it accepts.

Contrast that to the above snippet. That's 1 fn, which accepts an impl Trait i.e. a specific-but-unknown type and does something with it based on Trait. It can't do anything else with it other than indicated by Trait¹, e.g. it can't access any fields or inherent methods.

So why does the snippet appear to work? Well, behind the screens the foo fn is monomorphized i.e. a version of foo is generated for one of the int types², one of the float types, and &str. Isn't that back to overloading then? Well, no, not entirely at least. That's because foo is still limited in what it can do with its argument.

¹ Trait here actually means "one or more traits", but that doesn't change the point above.
² I'm not entirely sure what the inferred type of the 1 in foo(1) is. An obvious choice would be u8 to minimize space usage.

3 Likes

That crate uses a clever workaround to simulate function overloading by implementing the Fn* traits for a struct. It isn't like nightly has a feature toggle that turns on function overloading.

3 Likes

Abstraction without overhead (which people already refered here) calls it overloading.

Precisely the same happens here. Consider that extended example:

fn foo(x: impl Foo) {
    Foo::foo(x)
}

trait Foo {
    fn foo(self);
}

impl Foo for i32 {
    fn foo(self) {
        println!("This is int: {self}")
    }
}

impl Foo for f64 {
    fn foo(self) {
        println!("This is double: {self}")
    }
}

impl Foo for &str {
    fn foo(self) {
        println!("This is &str: {self}")
    }
}

fn main() {
    foo(1);
    foo(4.2);
    foo("Hello");
}

What limitation has this style of overloading may impose? Practically-speaking I can see only one: instead of writing foo(1);, foo(1, 2);, foo(1, 2, 3); you have to write foo(1);, foo((1, 2));, foo((1, 2, 3)); (on stable, on nightly even that is fixable).

Yes, it has. For full-blown overloading you need this flexible conversion between single tuple and multi-argument function. That conversion is implemented in Rust, but only for special, “magical” traits Fn/FnMut/FnOnce.

And yes, it still leaves some plausible explanations for why it wouldn't be desirable to implement full-blown overloading (e.g. one may argue that supporting variadic templates would be better than supporting this automatic conversion for arbitrary function calls), but it shows that issues with overloading are political, not technical.

It's not an accusation. Political does not “made by politics”. Joe Biden or Mister Putin are not the only ones who can do political decisions. There are office politics, too.

The fact that they don't want to expose Fn/FnMut/FnOnce traits to stable Rust may be good or bad, but it's absolutely political decision, not technical one.

1 Like

That isn't saying much unfortunately.

For all practical purposes, you just named it: you manually need to involve a trait, implement it for various types etc.
Not impossible, but you don't get it automatically for free either.

But at that point, it's the Foo trait doing the polymorphism, not the foo fn. So while it can indeed be used to a similar effect, they're still different mechanisms.

My take on the words of @Cerber-Ursi was that when politics is named as a reason for something not happening, in general that has a negative connotation: that one or more stakeholders is either unwilling, or only willing for private gain.

Importantly, neither is the case here.
If a feature isn't implemented, it's either due to an active decision on technical grounds, or it just hasn't been implemented because people both willing and able to work on rustc are relatively scarce.

What are you basing that on? Who is unwilling, or trying to obtain personal gain in exchange for this feature?

3 Likes

The reason isn't technical, it could very likely be done, it is more that overloading is a dubious idea from a human point of view. For one thing, when you want to refer a human to a function, you cannot just name it, instead you have to name it and specify the types of the arguments.

So there is endless possible confusion. Another example, if you have a list of functions names being displayed in documentation as hyperlinks, unless the arguments are also shown, until you click on the link you don't know which function you are going to get.

In summary, it is "potentially confusing" and the language designers considered it to be a bad idea (for Rust), and that is also the general consensus.

10 Likes

I think "political" is maybe not the right word here.

My understanding is they haven't stabilised Fn and friends because storing the arguments in a tuple means we'll be closing the door on variadic functions later on[1].

So it's a conscious decision made by humans for the sake of not accidentally painting ourselves into a corner later on.


  1. Or at least making the implementation a lot harder. I don't think a debate on possible solutions/workarounds would be helpful here. ↩︎

3 Likes

What rust doesn't have is ad-hoc overloading. It has principled overloading via traits instead -- that's how + can work on lots of types, for example.

It chose this because ad-hoc overloading doesn't mix well with type inference and generic code, whereas traits work great with both.

The pattern in Rust to do what your example looks like it's going for is "options" classes, like OpenOptions in std::fs - Rust.

9 Likes

I've found overloading on the number of arguments to be a reasonable approach; I guess we'll see what happens on that front, as it seems to be under evaluation already..

Even so, in eg. Erlang, where this routinely happens, you generally refer to functions like foo/1, foo/2 etc, indicating the number of arguments.

It depends. In C#, overloading based on number and type of arguments sometimes interacts in very surprising ways with optional, default-valued, and named arguments.

I'm quite happy with Rust's explicit appoach.

14 Likes

One piece this reminded me to emphasize: When you have overloading on types, you need overload resolution rules. These are by no means easy to get right, as Microsoft indirectly admits:

The overload resolution rules have been updated in nearly every C# language update to improve the experience for programmers, making ambiguous invocations select the "obvious" choice.

~ Improved overload candidates - C# 7.3 draft specifications | Microsoft Docs

(Those are their own scare quotes! I didn't add them.)

Since Rust has traits, Rust needs to have trait resolution rules, which are also non-trivial. (See the work around chalk, for example.)

It would be a nightmare -- both for programmers and for the language designers -- to have both complex overload resolution rules and complex trait resolution rules. Worse, if they needed to interact -- perhaps because of dyn Trait and impl Trait parameters -- it could get particularly ugly.

So since, as I previously mentioned, ad-hoc overloading mixes poorly with generic code and thus traits are needed anyway, Rust would rather just have the one mechanism for this kind of "choose the thing to call based on types" desire.

And thus, for example, Vec::extend is a trait that's implemented twice (https://doc.rust-lang.org/std/vec/struct.Vec.html#impl-Extend<%26'a%20T>), not a function with two different overrides.

10 Likes