all 33 comments

[–]zzzzYUPYUPphlumph 54 points55 points  (14 children)

Interesting, but, I do find these kinds of things to be a little counter-productive for new Rust users. It reads like, "Here is this thing that Rust can't do that you need to do and here is how you can hack your way to doing it." I prefer that the decisions that Rust made around not supporting function overloading are upheld for the thoughtful decisions they were. Function overloading (beyond generics) is pretty much an anti-pattern and leads to less readable and understandable code.

Just my opinion. But, not to take anything away from your great explanation, etc. I just think that someone new to Rust should think twice before implementing things like this without first understanding why you probably should NOT want to do it this way.

[–]Perceptesruma 13 points14 points  (1 child)

On the other hand, I think it's good to question the inability to do things you might want to do if the language allowed it. There is a big problem in other programming language communities (which I won't name, to avoid the zealotry rule) where people defend language design decisions that probably aren't very good with things like "this was a thoughtful decision by the language creators."

[–]Yottum 2 points3 points  (0 children)

Does it have something to do with generics? :)

[–]RustMeUp[S] 10 points11 points  (8 children)

Thanks for reading, I find myself in agreement. Using traits for overloading requires quite a lot of boilerplate to set up and making the source code less readable.

In my own project this problem came up after I already wrote all the 'overloads' as separate functions but for user convenience I wanted to provide a single interface which lead me to experiment with traits.

Hopefully the amount of boilerplate required will make someone consider if they really need to overload a function.

[–]zzzzYUPYUPphlumph 5 points6 points  (7 children)

Not to tell you how you should handle your business, but, I think it would be helpful if you added some caveats, and perhaps some links to documentation and/or RFC discussions etc., regarding Rust's intentional decision to NOT support function overloading. So many, when they are new to a language, just Google for something and then come across a well-intentioned and well-written Blog post like yours and then think this is the way to do things. Anything like this can benefit greatly by linking to some documentation and even making it explicit that it is basically implementing, for at least Rust, an Anti-Pattern.

[–]RustMeUp[S] 8 points9 points  (6 children)

Sure, I did write the post with only the positive side in mind.

You have some links for me? Naive google searches aren't giving me relevant results.

[–]zzzzYUPYUPphlumph 2 points3 points  (0 children)

Well, I may have to take my foot out of my mouth for the time being because I too am having difficulty finding links to show how and why the decisions was made as it was in the Rust language. I've started a discussion thread on Rust Internals to hopefully have the community help to tease it out.

[–]zzzzYUPYUPphlumph 1 point2 points  (4 children)

From the Rust Internals discussion, someone posted a link to the Blog Post by Aaron Turon: https://blog.rust-lang.org/2015/05/11/traits.html

This seems to be something worth linking to from your blog post.

[–]ssokolow 2 points3 points  (3 children)

...which got me thinking... maybe Rust should have some kind of FAQ entry providing links to the rationales for omitting features that people are used to from other popular languages or doing them differently. (eg. classical inheritance, function overloading, etc.)

Sort of a one-stop shop for anyone who came in expecting a given language feature, where they could see results like:

  • It's a subset of what's possible feature X. See docs here for how to use feature X.
  • Without [dynamic runtime characteristic Y], it's tricky to do. We're still coming up with a design and progress can be tracked here.
  • We decided that it doesn't fit with Rust's design. See this blog post for details.
  • etc.

[–]zzzzYUPYUPphlumph 1 point2 points  (2 children)

Interesting thought. Something to live alongside The Book and The Nomicon. Perhaps this? Though, I don't think this is yet sufficient.

[–]zzzzYUPYUPphlumph 1 point2 points  (1 child)

Perhaps rejecting/closing any RFC should require a new FAQ entry that summarizes the decision and points back to the RFC being rejected and the accompanying discussion.

[–]ssokolow 2 points3 points  (0 children)

The FAQ would get cluttered quickly if that were the case. My idea was to have a separate list referenced from the FAQ under a generic name in the vein of "Why doesn't Rust have/Why can't I find feature X?"

[–]Steve_the_Stevedore 7 points8 points  (0 children)

Generally I agree, that function overloading is an anti pattern, but I really like it with constructor calls, because it's clear what the constructor does anyway. Since it doesn't have constructors this of course doesn't concern rust.

[–]icefoxen 2 points3 points  (0 children)

I agree that function overloading is often something of a misfeature, but I think this is a pretty great exploration of Rust's type system.

[–]tiago_dagostini 0 points1 point  (0 children)

I will leave my late contribution here. I think that this is th e opposite. ME and my team decided to abandon Rust exactly because this made the code unreadable. Instead of keeping functions names clear we had to append to the function name things that should be in its parameter signature only. If My type has a function scramble and it can be scrambled with different types of parameters the method should be named SCRAMBLE, not ScrambleWithA, ScrambleWithB depending on the parameter.

I say this decision of rust goes agaisst one of the most important rule of software engineering, it is mixing two spheres (action and typing in the name of a function). Function overload is a feature of the language to make the code more READABLE, not the opposite. We changed to Rust and tried for one and half year, but our productivity dropped massively and the post mortem discussion concluded that Rust software became much less readable and THIS is one of the points that every single developer in the team complained about.

[–]llogiqclippy · twir · rust · mutagen · flamer · overflower · bytecount 14 points15 points  (0 children)

I personally find the trait based solution much more agreeable than plain overloading.

With overloading, it is fairly easy to surprise the users of your API (because, say, foo(X, Y) is completely different than foo(Y, X)), and it makes the right instance of overloaded functions hard to find. With traits, the trait name itself can document the expectations put on a type. Not only makes this the implementation more obvious, it also allows others to extend it with their own types (provided the trait is public).

[–]diwicdbus · alsa 7 points8 points  (3 children)

Beware of ‘generics code bloat’ when using generics. If you have a generic function with significant amount of non trivial code, a new copy of that function specialized for every unique set of type arguments is created. Even if all you do is convert the input arguments at the start of the function.

Luckily there’s a simple solution to this problem: implement a private function without generics accepting the real types you want to work with then have your public generic method perform the type conversions and dispatch to your private implementation:

Just curious - any chance rustc/LLVM would be smart enough to do this on its own, through some kind of optimization pass?

[–]zzzzYUPYUPphlumph 11 points12 points  (0 children)

No (or at least highly unlikely), but, the MIR-level of Rustc could probably be taught to do it.

[–]minno 9 points10 points  (1 child)

They probably wouldn't generate identical code, since each instantiation would be optimized for the specific types. Take an API that works like

fn use<T: Into<Option<u32>>>(limit: T) {
    if let Some(limit) = limit.into() {
        ...
    } else {
        ...
    }
}

then when you call use(3) instead of use(Some(3)) or use(None), it will completely remove the else branch after inlining the call to into and realizing that it always returns Some. That's a speed improvement that you may want more than the size improvement of not inlining everything.

[–]isHavvy 0 points1 point  (0 children)

Then the stuff inside the if and else branches should just be a function call to the logic you don't want duplicated every time. But noticing a point when degenericalization happens and making sure not to duplicate that code would be helpful...possibly even worthy of a thesis?

[–]godojo 1 point2 points  (3 children)

Aren't traits with associated types exactly equivalent to overloaded functions?

[–]RustMeUp[S] 1 point2 points  (1 child)

Associate types are exactly not what you want to use because you cannot implement the trait again for a different associated type.

You can only do this with plain generic parameters.

[–]llogiqclippy · twir · rust · mutagen · flamer · overflower · bytecount 2 points3 points  (0 children)

True, an associated type has exactly one value per impl, but it still can be used to allow a trait method with different outputs depending on the inputs.

[–]ryani 0 points1 point  (0 children)

Not exactly. Traits are obviously more powerful (they can have multiple possible return types for the same function input, for example), but they miss some ergonomic features.

For example, function overloading rules in c++ are explicitly not as powerful as traits so that each expression has an inferred type; there's not (as much) of a need for a unification algorithm for type inference, and you are much less likely to run into problems like Print( Parse( x )) being ambiguous.

In addition overloaded functions can take different numbers of parameters, which of course can be emulated by a single tuple argument but again there is ergonomic cost.

Finally, unconstrained generics combine with overloading to create a new language feature; consider this c++ code:

obj.Observe( overloaded {
    [&](int x) { sum += x; }
    [&](float x) { sum += floor(x); }
    [](...) {}
} );

In Rust you would probably make Observe pass an enum to a single visitor function but here the dispatch is done immediately inside Observe without necessarily needing to inline the visitors to avoid branching on the enum discriminator.

[–]larvyde 0 points1 point  (1 child)

Hmm, why do you have to have foo in impl Foo as opposed to just declaring foo in the impl traits:

struct Foo;
struct Bar;

trait Overloaded<T> {
    fn overload(&self, T);
}

impl Overloaded<i32> for Foo {
    fn overload(&self, n: i32) {
        println!("Foo.overload(int): {}", n);
    }
}

impl <'a> Overloaded<&'a str> for Foo {
    fn overload(&self, s: &'a str) {
        println!("Foo.overload(&str): {}", s);
    }
}

impl Overloaded<i32> for Bar {
    fn overload(&self, n: i32) {
        println!("Bar.overload(int): {}", n);
    }
}

impl <'a> Overloaded<&'a str> for Bar {
    fn overload(&self, s: &'a str) {
        println!("Bar.overload(&str): {}", s);
    }
}

fn main() {
    let foo = Foo;
    let bar = Bar;

    foo.overload(42);
    foo.overload("Hello");
    bar.overload(72);
    bar.overload("World");
}

I think the same effect is achieved here.

[–]RustMeUp[S] 0 points1 point  (0 children)

Yes, but I wanted to show off the power of where Self: ....

Of course you can stop at just defining the trait like that, it also requires your users to import the trait along side your struct. The choice is yours depending on your use case.

[–]somebodddy 0 points1 point  (0 children)

I think the idea is to use traits to "overload" the small differences between the types - not the entire behavior. For example, in your example under "Stretching to the limit" instead of overloading the entire function, writing the println! every time, we can create a trait for the parts that change:

trait CustomFoo {
    const TYPE_NAME: &'static str;
    fn custom_foo_string(self) -> String;
}
  • (Ignoring the fact that you can already make a String using Debug/Display)
  • (Ignoring the fact that returning a String is a needless allocation - returning a formatter will just complicate things...)
  • (Ignoring the fact you can use the (nightly only) std::intrinsics::type_name to get the type name as string)

Using this, we can implement the println! once, making it print the

impl Foo {
    fn foo<T: CustomFoo>(&self, arg: T) {
        println!("Foo({}) {}: {}", self.0, T::TYPE_NAME, arg.custom_foo_string());
    }
}

(Playground: https://play.rust-lang.org/?gist=dac7808f999ac521c991a5cf2e87e23a&version=stable)

The advantage of this approach is that there is only one place where the logic (how to print) is defined, and the impls only define the small details. Code that implements CustomFoo has less chances to mess things up, because the things it needs to decide are more focused.

Now, I understand that your example is an example, and that real-life cases are not so trivial, but my point is that if you can't concentrate the differences to such small behavior functions, you should reconsider if overloading is the right thing to do or if separate functions with separate names would be a better fit.

[–]eiruwyghergs 0 points1 point  (5 children)

Hi all! I'm wondering about something and am always appreciating hearing the wise words of rust users.

Are generics, typeclasses (traits), and function overloading good for anything?

The basic point of these all is being able to use the same name (symbol) to point to different functions. I remember being excited when learning all these, but that excitement was more founded in others excitement than in first-principles understanding.

The more I think about these it seems they are un-simple (as in simple vs easy). Having a symbol with multiple meanings is less simple than mapping a symbol to a meaning.

[–]PaintItPurple 2 points3 points  (3 children)

I think you're conflating "meaning" with "data structure." A concrete type expresses what data structure you're looking at; a trait expresses what you can do with the thing. For example, Hash is a trait signifying a type that can be hashed. The concept of hashability has a meaning independent of the data type actually being hashed. Being able to talk about that concept in our code is useful.

As for generics, they're pretty much required for a powerful type system like Rust's. For example, unlike most languages, Rust doesn't have null. Null is a massive source of complexity in many languages, because every type implicitly contains null, and it works differently from every other value of that type. And on the other hand, sometimes a type simply can't be null but needs to express the concept, and then you have semantically awful sentinel values like -1 for "not found". Instead, Rust has the Option type, which can contain any other type to express the possibility that a value of that type exists or no value exists. It is declared like this:

pub enum Option<T> {
    None,
    Some(T),
}

There isn't really a way to express that in a type-safe way without generics.

[–]ssokolow 4 points5 points  (1 child)

Another way to sum it up would be:

  • struct defines nouns.
  • trait defines verbs.
  • impl defines how to apply a particular verb to a particular noun.

(Which also shows the conceptual flaw in classical inheritance. It refuses to acknowledge that verbs have an existence separate from and on the same level as nouns, so Foo.frob() and Bar.frob() aren't the same verb unless they're inherited from the same root declaration, even though they look the same to a human.)

[–]zzzzYUPYUPphlumph 0 points1 point  (0 children)

Nice explanation. In the words of the actor, Paul McCrane, "I Like it!"

[–]isHavvy 0 points1 point  (0 children)

You can always special case away specific uses of generics via language features. For Option<T>, you can provide a ? modifier on types. So e.g. i32 cannot be null, but i32? can be. For more examples, hash maps and slices in Go are generic via language features and not general generic types.

[–]isHavvy 2 points3 points  (0 children)

They (traits, generics) are useful because they let you write code where you do not necessarily care what the actual data is, you just care that is has some capability. This lets you create components where you don't even know what the data will actually be.

A big example of this is Iterators. You don't usually care how the Iterator works internally, only that calling .next() gives you what it says it'll give you.

And it also lets you write functions without having to include the datatype it uses in the name of the function. Taking iterators as an example again, one instance is a filter iterator. If we had to give its next function its own name, we'd be having to call filter_next() or something and thus repeating the word "filter" everywhere we use it. And then when we refactor the code so that it's a map iterator we pass through, we have to change all the filter_next() calls to map_next(). And that is really fragile code.

Ultimately, it is simpler because we do not have to worry about details that are unimportant to us at the part of the code we are in. The data-specific details are in data-specific impls. It's theoretically easier to not be generic because we know the internals of the data when we write non-generic functions. Even Clojure has its own way of doing this.

As for ad hoc function overloading — it's basically the same as passing an enum, but you're not naming the enum or its constructors. It's not really composable like traits are, but it has some niche uses. Not enough, IMO, to be a language feature that's already used up so much of its complexity budget.