Rust Function Overloading - Call for Experimentation

36 points by madsmtm 19 hours ago on lobsters | 26 comments

junon | 18 hours ago

I actually hope this doesn't become more ergonomic, or at least, not for Rust code. Overloading is a huge pain in the side of C++ codebases where it's unclear of the function's behavior just looking at the arguments being passed in, even if you're familiar with the function(s) that can be called. It's one of the explicitness items of Rust I really appreciate.

This feels like it's going to be abused quite a bit despite being for C++ primarily, in code that doesn't touch C++ at all.

ssokolow | 17 hours ago

Agreed... plus, from what I remember, template overloading was one of the culprits which resulted in C++'s terrible error messages, and function overloading in Rust was previously rejected because of how it interacts with type inference.

I don't have a problem with overloading as a concept. It works well enough in C#, Java and Ada.

In C++ implicit conversions and rampant templating can heavily obscure which function flavor gets called. Someone wrapped something in a macro that uses a template, which calls an overloaded function, and the argument has a user-defined implicit conversion (or non-explicit constructor) I don't know about, so me, my IDE, and the LLM are like "WTF is happening!?" as we start untangling this tangled ball of Christmas lights.

junon | 16 hours ago

I agree they work fine in those languages. That being said.

  1. At least for C# and Java, the tooling there is immensely good since the languages are a bit simpler (than C++) and we're more or less built with tooling / IDEs in mind.
  2. This is Rust<->C++ in particular, which is a nightmare scenario given C++ tooling itself is lackluster (and has been for decades).

Rust code can, for the most part, be written without an IDE, assuming you have docs available. All languages can of course, but the efficiency gains you get from C# and Java stem from tooling, whereas in Rust it's the compiler and its errors. This is in large part because Rust doesn't require you track down conditionally-implemented types; you can almost always track figure out what's happening, exactly, based on the file alone. Traits muddy this property a bit but the defacto culture of naming traits based solely on what they do helps. Otherwise, the lack of overloading is doing a lot of heavy lifting in terms of readability.

ssokolow | 6 hours ago

Rust code can, for the most part, be written without an IDE, assuming you have docs available

And, at least historically, that was a stated design goal, brought up in RFC discussions, on the principle that the higher your baseline usability, the more room an IDE has to raise the roof for what a user actually experiences before it starts to struggle to push it further.

I don't use overloading in C# or Java, because I don't think it works well as a concept. For every place that I could overload, I find that it's usually better to give functions a different name.

I've never used Ada for more than a few hundred lines of toying around.

landon | 11 hours ago

I'm kind of confused. Is this for c++ FFI specifically? If so, what's wrong with explicitly name mangling exports? The cxx crate and i think others have utilities to make it easy. I don't think FFI ergonomics is a strong enough justification for a language feature this deep.

I personally kind of like that rust doesn't have overloading, in C++ I'm never sure i'm looking at the right implementation because of it. Between macros and Into/From i think we've got a better dispatch mechanism for everything overloading does well.

gunduzc | 18 hours ago

Extending it to native Rust could be a separate feature, stabilised on a longer timeframe (or not at all).

I mean, I don't understand why native Rust would need this. Aren't traits the right way to do this? (If I recall correctly?)

mitsuhiko | 17 hours ago

Variadic overloading is not possible with Rust and might be quite ergonomic. It’s trickier when it involves different types and defaults.

I’m not sure where the balance is but I don’t think Rust found it today. In particular I think keyword arguments should be in scope.

zesterer | 16 hours ago

For me, the key pain point today is the lack of named arguments + defaults. I really do not want a world in which a callee's implementation can change based on the number or type of arguments the caller passes: it massively hurts greppability. That Rust kept away from C++ style constructor overloading and has a strong culture of differentiated constructor methods is one of the big things I love about the language.

mitsuhiko | 16 hours ago

I really do not want a world in which a callee's implementation can change based on the number or type of arguments the caller passes: it massively hurts greppability.

I think it's complicated. For you the issue might be the number of arguments, but today's Rust code already allows you to dispatch based on type and we seem to be fine with it? (as in you can have one function that is overloaded based on the type of argument).

trait Do {
    type Output;
    fn do_something(self) -> Self::Output;
}

impl Do for &str {
    type Output = String;
    fn do_something(self) -> String {
        let rv = self.to_uppercase();
        println!("{rv}");
        rv
    }
}

impl Do for i32 {
    type Output = i32;
    fn do_something(self) -> i32 {
        let rv = self.pow(2);
        println!("{rv}");
        rv
    }
}

fn do_something<T: Do>(value: T) -> T::Output { value.do_something() }

fn main() {
    let text: String = do_something("hello from rust");
    let number: i32 = do_something(12);
    println!("String length: {}", text.len());
    println!("Squared number plus one: {}", number + 1);
}

zesterer | 12 hours ago

This is sort of besides the point, right? The whole purpose of traits is to allow polymorphic dispatch, that's sort of the whole idea. Traits that didn't allow that would be useless. That doesn't mean that polymorphic dispatch is a good thing to wire into other aspects of the language too. Traits are deliberately heavyweight and syntax-heavy because Rust is trying to tell you "don't do this unless it's actually necessary".

mitsuhiko | 10 hours ago

My point is that you can already overload functions by type with the help of traits. You just cannot overload them by number of arguments (only if you use a single argument that is a tuple).

zesterer | 10 hours ago

mitsuhiko | 9 hours ago

But you cannot make this work for methods. And if you were to place this as a struct member for fun, yo need to call it with (object.method)(...).

zesterer | 9 hours ago

I'm not being serious. Although, I think it's neat to see that there's no reason in principle that the type system couldn't be convinced to support it.

I don’t think that the existence of similar complexities is a particularly great justification for adding complexity. And Rust is already a language struggling with complexity.

junon | 16 hours ago

It'd be nice for tuples in particular to get some ergonomics (a lot of macro boilerplate exists for "tuples up to N elements" impls) but beyond that I think this is a recipe for disaster.

mitsuhiko | 16 hours ago

I think this goes well beyond tuples. For instance have a look at the trait infrastructure in minijinja. It already today allows you to register variadic functions and invoke it (by piggybacking on top of tuples) but the code necessary to support internally is pretty gnarly.

Lack of overloading is one of the things that annoys me about Rust syntax.

It’s not unusual for a type to have multiple methods that do conceptually the same thing but take different arguments. With overloading you can name the single “thing”, the operation, but have overloads for the different cases. Without it, you have to give each set of arguments its own method name, and then the programmer has to remember all those names and which does which. This results in longer (often awkward) names, and more mental overhead.

kornel | 3 hours ago

With overloading you can name the single “thing”, the operation, but have overloads for the different cases

To me this is the worst downside. You have different cases under the same name, so you can't see which case the code is calling!

Overloading by arg type in a language with type inference is asking for trouble. Rust's trait-based approximation of it already works poorly. It can break type inference and you need to add explicit type, which is more of a syntax eyesore than adding _with_x to the name. Sometimes it magically picks i32 or () or &&T you didn't expect.

I wouldn't mind very limited overloading by number of function arguments only (or named/optional args or ObjC-like trick for them).

To me this is the worst downside. You have different cases under the same name, so you can't see which case the code is calling!

In a decent API this isn’t a problem. And IDEs will show you the exact method if you hover over the name.

Overloading by arg type in a language with type inference is asking for trouble. Rust's trait-based approximation of it already works poorly.

Swift and Kotlin manage to do it…

Swift wouldn't have been my goto example for a language without type inference issues.

gignico | 7 hours ago

Indeed. People are worried about readability in the presence of abuse, but that is a problem with any language feature.

ssokolow | 6 hours ago

Honestly, I'd point to how Iterator::collect overloads return types as proof that overloading, as implemented by the people best positioned to use it well, is already making life suffering in Rust in the one place that it's allowed.

I don't want my workflow to become any more "IDE is mandatory" than "add : Vec<_> to my let and then let Clippy suggest alternative return types I didn't remember existed".

Rust has historically been as good as it was in no small part because RFC discussions treated "becoming like C++" as a boogeyman. An ML-lineage language, garbed in C++ syntax, trying to avoid becoming any more like C++.

gignico | 19 hours ago

Indeed a bit of function overloading would be useful in Rust. But I come from C++ so I don’t know how much biased I may be.

kevinc | 3 hours ago

Comment removed by author