Why Rust doesn't have function overloading?

It's the same situation with Scala, Baal forbid when you interact with Java code :sweat_smile:.

2 Likes

I can't speak for the reasons, but consider the following; what should be printed?

struct Foo(String);
impl Foo {
   fn new(x: &str) -> Self { println!("&str"); (x.to_string()) }
   fn new(x: impl AsRef<str>) -> Self { println!("&AsRef<str>"); (x.as_ref().to_string()) }
   fn new(x: impl Into<&str>) -> Self { println!("&Into<&str>"); (x.into().to_string()) }
   fn new(x: &[u8]) -> Self { println!("&[u8]"); (x.as_string().unwrap().to_string()) }
   fn new(x: impl AsRef<[u8]>) -> Self { println!("AsRef<[u8]>"); (x.as_ref().into_string().to_string()) }
   fn new(x: impl Box<&str>) -> Self { println!("Box<&str>"); (x.to_string()) }
   fn new<'a>(x: impl Cow<'a, str>) -> Self { println!("Cow<&str>"); (x.to_owned()) }
   fn new(x: String) -> Self { println("String"); (x) }
   fn new(x: &String) -> Self { println("&String"); (x.clone()) }
   fn new(x: Box<String>) -> Self { println("Box<String>"); (x.into_inner()) }
}

(I probably had syntax errors). Currently in rust, EACH of the above would have to be it's own function name. Many of the types are AMBIGUOUS and redundant. AsRef, &, Box, etc. If (like in C++) the compiler was forced to coerce a type into an existing overloaded function, you have a large degree of surprise. I have encountered such surprise-bugs in C++; a code-review is useless without a PHD and a glass a wine. You'd need to actually RUN it to guess correctly.

Alternatively you can use a macro; the macro can then pattern match ALL of the above and "do the right thing" - and the compiler can just keep having a simple, consistent set of rules that a caller can infer. Further, the macro has salience; the FIRST pattern that matches will always win.. In C++, depending on the include-file order (which might be different in each ".cc") it's POSSIBLE an implementation might produce a DIFFERENT target - I don't know; I could be speaking out of my ass, but I'm sure that would be possible in Rust at least if implicit function overloading were implemented like C++ (but everything else was kept the same).

Not knocking function-overloading; I too find it missing at times (producing bizzar function names to add clarity, but struggling with what to call it).

6 Likes

Not necessarily. I have only done four of your functions, but you get the idea:

struct Foo(String);
impl Foo { fn new(x: impl NewFoo) -> Self { x.new() } }

trait NewFoo { fn new(self) -> Foo; }

impl NewFoo for &str { fn new(self) -> Foo { println!("&str"); Foo(self.to_string()) } }
impl NewFoo for String { fn new(self) -> Foo { println!("String"); Foo(self) } }
impl NewFoo for &String { fn new(self) -> Foo { println!("&String"); Foo(self.clone()) } }
impl NewFoo for Box<String> { fn new(self) -> Foo { println!("Box<String>"); Foo(Box::<String>::into_inner(self)) } }

Why? Overloading in Rust differs from overloading in C++ the same way generics in Rust differ from templates in C++: while C++ defers ambiguity resolution till call time (called instantiation time for templates) Rust insists on resolution of conflicts at define time.

That's why I could't implement what you write fully: some of your definitions conflict.

This makes overloads in Rust much safer. The only issue is the fact that you can't pass many arguments to your new function (but can pass a tuple!).

Oh, absolutely. I like overloads in Rust much better than in C++. The only issue is lack of support for multiple arguments.

Of course if you add some random crazy feature which would behave in random crazy way you may get crazy results.

There are no implicit overloads in Rust and that's fine. Rust does support explicit overloads and that's pretty cool, really.

If you would do what compiler does with Fn/FnMut/FnOnce today by adding couple of additional braces when you need multiple arguments you can see how hypothetical extension would behave, too. On nightly you can use overloadable and see how the whole thing works.

I missed it till I read about how it's supposed to work. Now I only miss the ability to pass multiple arguments.

Apparently this is kept unimplemented to make it possible to implement variadic generics is some bright future. Not sure how that coercion may hurt… but maybe there are some dark deep reasons, IDK.

2 Likes

I just hit a perfect example in C# of why Rust doesn't have function overloading. Full runnable example at https://dotnetfiddle.net/DrpfpE; below will have snippits.

C# doesn't have much inference, but it added a tiny bit more in C#9 to let you call constructors without writing out their type, if it can figure it out from context on the same line. That means that you can write this:

  public static void DoStuff(Options options) {
	System.Console.WriteLine("yup");
  }

  public static void Main() {
     DoStuff(new() { Foo = 123 });
  }

And that works great.

But if you try to add an overload, even one that "obviously" wouldn't work with that call

  public static void DoStuff(string blah) {}

then it stops compiling

The call is ambiguous between the following methods or properties: Program.DoStuff(Options) and Program.DoStuff(string)

So you get type inference or ad-hoc overloading for DoStuff, but not both. And I'd take type inference over ad-hoc overloading every day.

10 Likes

The same reason you don't name your wife, daughter, dog, parakeet, and exercise routine "fubar".

You're moving the thinking problem from the writer to the reader.

I have the same opinion of "shadowing".

So here on page 42 of this mess this fubar thing, was it set way up there to a String? Or was it an i32? Oh wait. It was a reference to that blasted closure set at the top. Those other ones were conditionally skipped.

KISS is already dead. Quit shooting the corpse.

In a language without types and with parameter hoisting¹ like Javascript, I completely agree. It just invites spaghetti code.

However, rust is opposite Javascript in both those dimensions. What this translates to is that there is no situation where in one branch a binding is one value of type T, and in other branch its another value of type V. In fact those branches must unify to the same type if the binding is not defined within either branch. And in that case it would be a new binding completely.
And of course Rust is typed, and types are a limited form of compiler verified documentation on the code.

There's an interesting flaw in this approach, though. A call like this one will not be able to infer its type:

fn main() {
    Foo::new("example".into());
}

error[E0282]: type annotations needed
  --> src/main.rs:11:24
   |
11 |     Foo::new("example".into());
   |                        ^^^^

Playground example

When people say that type inference and overloading don't work well together, they're talking about this sort of thing, where overloading a function necessarily makes using that function more complicated than it would be if each overload had its own name.

8 Likes

Which makes it all the more ironic when you recall that despite that issue Rust does support complicated case where one, single argument can be overloaded (and yes, this may lead to ambiguity, sure) yet much simpler case (functions with different arity) is not supported.

Yeah, that probably would've worked with type inference. Like most features that other languages have and Rust doesn't, it was discussed before:

According to the triage report, it was uneventfully postponed.

I suppose the main reason it never happened, at least based on searching the old ML archives for mentions of the word "overload", is just lack of demand. I can't find anyone beyond this RFC that's asking for arity-based overloading, compared to the way there's always someone expressing a desire for full-blown function overloading every couple years.

Sure, arity-based overload would be easier to add than arbitrary type overloading, but it still requires some design work, and it doesn't solve as many problems as arbitrary type overloading.

The main language I know of that has arity-based overloading is Erlang, and it has pattern matching in function signatures to simulate type-driven overloading.

3 Likes

True. But there's also the fact that you can almost use it today (by just doing foo((a, b, c)) instead of foo(a, b, c) — yes, that's tuple instead of multiple arguments, but close enough) which means one can already design API around it.

If these would be usable then converting these (by removal of extra parens) would be simple mechanical work.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.