Rust Function Overloading - Call for Experimentation
37 points by madsmtm
37 points by madsmtm
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.
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.
I agree they work fine in those languages. That being said.
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.
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.
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.
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++.
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?)
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.
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.
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);
}
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".
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).
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)(...).
I have no problem with that syntax personally. In fact I prefer it. It explicitly spells out to the reader that this isn't a method but a function-like field. That's useful information at a glance. It tells me the function itself isn't fixed, and that it's being passed in somewhere.
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.
One could argue that properly doing this would remove complexity. At least it's hard to say ex-ante without actually doing an experiment if this would make it better or worse.
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.
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.
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…
Possibility of mitigating some negatives still doesn't sound like a net positive to me.
I don't see where is the upside. Aesthetic feel? Saves typing a few characters?
Indeed. People are worried about readability in the presence of abuse, but that is a problem with any language feature.
The difference is it doesn't take abuse to make overloading unreadable.
One should at least concede that this take is a matter of taste. I’ve used C++ for 20years and I can read code with overloaded calls very well. The point is that overloaded functions should do the same thing just with different parameters. Of course it’s unreadable if instead you have two totally unrelated functions with the same name. But that’s just bad code, which is unreadable in any language.
It's a matter of taste, for sure, but it's not an ill-informed one. Prior to switching to Rust I was also a C++ dev for many years, and overloading has bitten me time and time again. Namely, integer promotion rules and other unexpected rules when overload selection takes place.
It's easy to dismiss bad overloading as "bad code" but the worst cases aren't the egregious cases, but the super subtle ones.
I also started to appreciate things differently after starting using Rust. Type promotions are orthogonal to overloading though. You may have function overloading in a language like Rust with no promotions at all and it would not lead to unreadability imho. Also as others have pointed out, the trait system is already powerful enough to have type-based overloading, we miss only the different number of arguments.
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.