Generics verbosity


#1

I’m a Rust, newb so don’t take this the wrong way. I want to learn what prevents following code:

fn max_by<B: Ord, F>(self, mut f: F) -> Option<Self::Item> 
    where
        Self: Sized,
        F: FnMut(&Self::Item) -> B,

From being slightly more like this:

fn max (self, mut f: FnMut(&Self::Item) -> Ord) -> Option<Self::Item>

I assume it’s something about monomorphization, although is it impossible to somehow determine which parts will be variable during it?


#2

Let’s look at simpler example:

fn foo1<T : Float>(mut f :T) { ... }
fn foo2(f : &mut Float) { ... }
fn foo2(mut f : Float) { ... }

The first is generic function and uses static dispatch. The second one takes trait object and uses dynamic dispatch. The third one does not compile. The parameter is instance of trait that has unknown size and currently does not exist in the language. If the semantics should be extended so that the third function is legal, there are two directions you can go:

  • Make it sugar for the first so it is monomorhized and uses static dispatch.
  • Allow passing of DST objects by value therefore it would use dynamic dispatch.

Now you can apply the same logic to your example.


#3

Think about a function that returns a reference to an object of some type A that implements Ord.

fn test<A: Ord>() -> &A;

Now lets try your notation:

fn test() -> &Ord;

This is currently legal rust. You can return a reference to an object of an unknown type that implements Ord. There’s a big difference. If you call the first function, you still know the type after the function call. If you call the second function, all you know is that the type of the object the reference points to implements Ord.

A similar reasoning applies to the function argument. In your case you are not using references, but together with some features that might come (like unsized return values), you’d end up with ambiguities.

Also: being explicit helps the compiler and imho the programmer a lot. Verbosity has some advantages.


#4

The postponed abstract return types RFC (aka “impl trait”) spawned discussion of features (IIRC, they weren’t in the RFC itself) that get close to your desired syntax, e.g.

fn max(self, mut f: impl FnMut(&Self::Item) -> Ord) -> Option<Self::Item>
     where Self: Sized

The main difference is the use of the impl keyword, which ensures there’s an explicit distinction between static vs. dynamic dispatch.


#5

What looks a bit confusing here is that something like Fn(Foo) -> Bar is not the closure type itself but rather a trait and there isn’t a syntax to specify the type itself which is anonymous and derived automatically by the compiler. So this leaks some implementation details of rust’s implementation of closures.
IMO this is something that bound to always confuse newcomers.

Regarding impl Foo for an unsized (DST) Foo - I don’t like this syntax as it doesn’t convey the actual semantics. In This case Foo is both a trait and a DST (object) type and seems we want to allow to make this distinction for the purpose of choosing type of dispatch.

I see two options that will be intuitive and clear for the user: Option A: specify the dispatch type as done in other language such as C# with a dynamic keyword which makes it clear what this actually does. pro: it is intuitive that the default is static dispatch and if you want to make it dynamic just use a dynamic keyword. con: new keyword add more complexity to the language

option B: use some variation of sugar to specify traits in function signatures such as trait Foo a-la C++ typename Foo. pro: this is intuitive and consistent - function signatures by default contain types and a modifier is required to specify a trait. This is also similar to C++ way so will be understandable for former C++ developers. Also there are similar proposals for C++ where a function with auto parameters is sugar for a template (gives static dispatch). con: the default dispatch with the keyword will be dynamic because the type is the DST object type.