New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

`dyn Trait` Syntax for Trait Objects: Take 2 #2113

Merged
merged 5 commits into from Sep 17, 2017

Conversation

Projects
None yet
@Ixrec
Contributor

Ixrec commented Aug 16, 2017

Introduce a new dyn Trait syntax for trait objects using a contextual dyn keyword, and deprecate "bare trait" syntax for trait objects. In a future checkpoint, dyn will become a proper keyword and bare trait syntax will be removed.

Rendered

The previous RFC on this topic is: #1603
The RFC that describes checkpoints is: #2052

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Aug 17, 2017

Contributor

👍 I believe this RFC will make it easier to write performant code because you can spot the dynamic dispatch locations more easily. Also, I think it will make learning Rust easier because it lays down a stricter difference between dynamic and static dispatch, making it easier for beginners to differentiate between the two.

Contributor

est31 commented Aug 17, 2017

👍 I believe this RFC will make it easier to write performant code because you can spot the dynamic dispatch locations more easily. Also, I think it will make learning Rust easier because it lays down a stricter difference between dynamic and static dispatch, making it easier for beginners to differentiate between the two.

@withoutboats withoutboats added the T-lang label Aug 17, 2017

@mark-i-m

This comment has been minimized.

Show comment
Hide comment
@mark-i-m

mark-i-m Aug 17, 2017

Contributor

I would rather &mut dyn Trait so that dyn always consistently comes before the trait name, which is easier to remember.

Contributor

mark-i-m commented Aug 17, 2017

I would rather &mut dyn Trait so that dyn always consistently comes before the trait name, which is easier to remember.

@ExpHP

This comment has been minimized.

Show comment
Hide comment
@ExpHP

ExpHP Aug 17, 2017

The first time this proposal came around it brought with it a bit of a kneejerk reaction; but it was one of those things where, the more you thought about it, the more you realized that it is absolutely, undeniably right! &Trait for trait objects was a mistake. It's a conceptual hazard and a performance hazard. At the end, my stance was that it is worth doing even if we do not repurpose bare Trait.

I will also confess that this RFC is what I had in the back of my mind the entire time as I read through the Checkpoint proposal.

So now that I've thoroughly voiced my stance of approval, I must ask: Isn't it a bit early? It feels to me like it was only just yesterday that the other RFC was closed (or maybe this is just me? apparently it was a year ago...), and the Checkpoint RFC hasn't even been merged. And I think some people are still trying to recover from all of the recent discussion on the module system...

I do want this to remain on the table, I just don't know if it's time to stoke that flame yet.

ExpHP commented Aug 17, 2017

The first time this proposal came around it brought with it a bit of a kneejerk reaction; but it was one of those things where, the more you thought about it, the more you realized that it is absolutely, undeniably right! &Trait for trait objects was a mistake. It's a conceptual hazard and a performance hazard. At the end, my stance was that it is worth doing even if we do not repurpose bare Trait.

I will also confess that this RFC is what I had in the back of my mind the entire time as I read through the Checkpoint proposal.

So now that I've thoroughly voiced my stance of approval, I must ask: Isn't it a bit early? It feels to me like it was only just yesterday that the other RFC was closed (or maybe this is just me? apparently it was a year ago...), and the Checkpoint RFC hasn't even been merged. And I think some people are still trying to recover from all of the recent discussion on the module system...

I do want this to remain on the table, I just don't know if it's time to stoke that flame yet.

@mark-i-m

This comment has been minimized.

Show comment
Hide comment
@mark-i-m

mark-i-m Aug 17, 2017

Contributor

@ExpHP Does have a solid point... There are already rather a lot of high-profile RFCs going around for the upcoming impl period...

Contributor

mark-i-m commented Aug 17, 2017

@ExpHP Does have a solid point... There are already rather a lot of high-profile RFCs going around for the upcoming impl period...

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Aug 17, 2017

Member

+1 for &mut dyn Trait. As I see it, &mut dyn Trait describes a mutable reference to a trait object, while &dyn mut Trait seems to suggest a dynamic reference to a mutable trait. It also keeps the dyn Trait syntax consistent, matching how it appears in &dyn Trait, Box<dyn Trait>, and impl MyTrait for dyn Trait { ... }.

Member

cramertj commented Aug 17, 2017

+1 for &mut dyn Trait. As I see it, &mut dyn Trait describes a mutable reference to a trait object, while &dyn mut Trait seems to suggest a dynamic reference to a mutable trait. It also keeps the dyn Trait syntax consistent, matching how it appears in &dyn Trait, Box<dyn Trait>, and impl MyTrait for dyn Trait { ... }.

@withoutboats

This comment has been minimized.

Show comment
Hide comment
@withoutboats

withoutboats Aug 17, 2017

Contributor

I definitely believe the correct grammar is &mut <keyword> Trait, since <keyword> Trait is the type. It would be as if we were to sometimes split an infinitive.

Contributor

withoutboats commented Aug 17, 2017

I definitely believe the correct grammar is &mut <keyword> Trait, since <keyword> Trait is the type. It would be as if we were to sometimes split an infinitive.

@Ixrec

This comment has been minimized.

Show comment
Hide comment
@Ixrec

Ixrec Aug 17, 2017

Contributor

&mut dyn Trait was my intent, &dyn mut Trait is just a typo.

Contributor

Ixrec commented Aug 17, 2017

&mut dyn Trait was my intent, &dyn mut Trait is just a typo.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Aug 17, 2017

Contributor

👍 from me.

Obviously, this has been stewing for some time. I think there is ample evidence that using Trait as a type is confusing and wasn't the best choice. Part of this arises, I think, from the limitations around dynamic dispatch (e.g., trait objects cannot be stored on the stack since their size is not known), so that people coming from other languages often try to use trait objects incorrectly when first coming to Rust.

I have also come around to the choice of keyword. dyn is short and I think the idea that "dyn Foo represents a value of some type T that implements Foo, but that type is only known at runtime (i.e., dynamically)" is reasonably clear. I also like the possibility of using dyn to indicate erased type parameters (discussed below). I think my second choice after dyn would be virtual, though it's rather long and perhaps more "jargon-y".

One question that might be worth discussing a bit: the terminology "trait object" is something that the compiler (and, I believe, some of the documentation) uses. For example, we say that traits are "object safe" when we can use them to make vtables. The RFC makes a rather brief argument against that terminology -- essentially, there are too many differences between "object-oriented systems" and "trait objects" for that to be a helpful intuition. I am not sure I fully agree with the argument, but regardless it seems like we should try to align our "English" terminology with the keyword we choose. If that is to be dyn, perhaps we want to refer to "trait object" types as something else -- but what? "dynamic traits"?

The RFC also opens up some interesting areas for the future. Obviously these would be future RFCs, but I did want to highlight two things I've been thinking about:

The first is the possibility of repurposing "bare trait" to mean impl Trait (which is discussed in the RFC). I am not sure yet what I think of this: it may be that having an active choice of which keyword to use ("dyn vs impl") when "converting" a trait to a type could be helpful when teaching and reading code (it helps to make clear whether a given name is a trait or a full-fledged type, for exaple). Overall, I think we'll find it easier to way the pros/cons once we have completed the transition to using dyn.

The second is the idea of permitting explicit erasure on type arguments to control monomorphization. For example, one might be able to do:

fn foo<dyn T: Debug>(x: &T) {
    ...
}

This would indicate a Java-like compilation strategy, where we all values of type T are known to be the same type, but we don't know statically what that type is. We would pass a single vtable at runtime to do dynamic dispatch. This would imply certain restrictions similar to today's object-safety rules but -- I think -- more permissive (e.g., it's ok to use Self in argument position, as long as you are not passing by value). If we supported unsized rvalues, those restrictions would be even fewer.

This latter use seems to indicate the value of making dynamic dispatch opt-in for types, since it allows us to use the same opt-in elsewhere.

Contributor

nikomatsakis commented Aug 17, 2017

👍 from me.

Obviously, this has been stewing for some time. I think there is ample evidence that using Trait as a type is confusing and wasn't the best choice. Part of this arises, I think, from the limitations around dynamic dispatch (e.g., trait objects cannot be stored on the stack since their size is not known), so that people coming from other languages often try to use trait objects incorrectly when first coming to Rust.

I have also come around to the choice of keyword. dyn is short and I think the idea that "dyn Foo represents a value of some type T that implements Foo, but that type is only known at runtime (i.e., dynamically)" is reasonably clear. I also like the possibility of using dyn to indicate erased type parameters (discussed below). I think my second choice after dyn would be virtual, though it's rather long and perhaps more "jargon-y".

One question that might be worth discussing a bit: the terminology "trait object" is something that the compiler (and, I believe, some of the documentation) uses. For example, we say that traits are "object safe" when we can use them to make vtables. The RFC makes a rather brief argument against that terminology -- essentially, there are too many differences between "object-oriented systems" and "trait objects" for that to be a helpful intuition. I am not sure I fully agree with the argument, but regardless it seems like we should try to align our "English" terminology with the keyword we choose. If that is to be dyn, perhaps we want to refer to "trait object" types as something else -- but what? "dynamic traits"?

The RFC also opens up some interesting areas for the future. Obviously these would be future RFCs, but I did want to highlight two things I've been thinking about:

The first is the possibility of repurposing "bare trait" to mean impl Trait (which is discussed in the RFC). I am not sure yet what I think of this: it may be that having an active choice of which keyword to use ("dyn vs impl") when "converting" a trait to a type could be helpful when teaching and reading code (it helps to make clear whether a given name is a trait or a full-fledged type, for exaple). Overall, I think we'll find it easier to way the pros/cons once we have completed the transition to using dyn.

The second is the idea of permitting explicit erasure on type arguments to control monomorphization. For example, one might be able to do:

fn foo<dyn T: Debug>(x: &T) {
    ...
}

This would indicate a Java-like compilation strategy, where we all values of type T are known to be the same type, but we don't know statically what that type is. We would pass a single vtable at runtime to do dynamic dispatch. This would imply certain restrictions similar to today's object-safety rules but -- I think -- more permissive (e.g., it's ok to use Self in argument position, as long as you are not passing by value). If we supported unsized rvalues, those restrictions would be even fewer.

This latter use seems to indicate the value of making dynamic dispatch opt-in for types, since it allows us to use the same opt-in elsewhere.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Aug 17, 2017

Member

the terminology "trait object" is something that the compiler (...) uses

Do note that a trait object is a TyDynamic containing one or more ExistentialPredicates.
You could say I was anticipating a chalk-based world where we could potentially encode existential types accurately - either way, I'd like to move in the direction of "dynamic existentials" in rustc.

Member

eddyb commented Aug 17, 2017

the terminology "trait object" is something that the compiler (...) uses

Do note that a trait object is a TyDynamic containing one or more ExistentialPredicates.
You could say I was anticipating a chalk-based world where we could potentially encode existential types accurately - either way, I'd like to move in the direction of "dynamic existentials" in rustc.

@stevenblenkinsop

This comment has been minimized.

Show comment
Hide comment
@stevenblenkinsop

stevenblenkinsop Aug 17, 2017

If that is to be dyn, perhaps we want to refer to "trait object" types as something else -- but what? "dynamic traits"?

"Dynamic trait types/values". "Dynamic traits" sounds like a feature of the traits themselves, rather than just of the types/values. Of course, I'm so used to "object types" that I don't see the problem either ;).

The other related term is "object safety". It seems like the term would become something like "dynability". It could be problematic with dynamic type parameters because there would two levels of dynability. I suppose dynamic type parameters could require "erasable" traits instead.

stevenblenkinsop commented Aug 17, 2017

If that is to be dyn, perhaps we want to refer to "trait object" types as something else -- but what? "dynamic traits"?

"Dynamic trait types/values". "Dynamic traits" sounds like a feature of the traits themselves, rather than just of the types/values. Of course, I'm so used to "object types" that I don't see the problem either ;).

The other related term is "object safety". It seems like the term would become something like "dynability". It could be problematic with dynamic type parameters because there would two levels of dynability. I suppose dynamic type parameters could require "erasable" traits instead.

@alexcrichton

This comment has been minimized.

Show comment
Hide comment
@alexcrichton

alexcrichton Aug 17, 2017

Member

I'm personally not necessarily against this RFC but not necessarily for this RFC. My opinion purely stems from compile times in Rust today, and let me elaborate!

Two of the headline sections of the motivation section in this RFC right now are:

  • favors a feature that is not more frequently used than its alternatives
  • favors a feature that ... is sometimes slower

And these are correct! It's pretty rare to see trait objects today and they are indeed factually slower than inlined methods in any possible microbenchmark. My thinking, though, is that this comes at a very real cost to compile times in the compiler.

The number one source of compile time slowness today is, I believe, due to monomorphization. This is because whenever you call a generic function we have to generate LLVM IR for that function call (each time it's called) and LLVM also has to optimize it and generate code for it. On small scales this doesn't matter too much but I'd like to talk about Rust code "in the large" for a moment.

I'd like to start out with some data I've collected to try to confirm this hypothesis. I wanted to analyze the top thousand crates on crates.io and evaluate how much code the compiler translates specifically for that one crate and specifically for upstream crates. That is, if you call a generic function in the standard library, like mpsc::channel(), that's "std's fault" whereas the main function of a crate is "your fault".

For each crate on crates.io I emitted the IR for the final crate. For each function in this final crate I classified each LLVM function as "from dependencies" or "local" depending on the debuginfo (looking at the filename). I then counted the total number of lines of LLVM IR to attribute to "from dependencies" and "local".

And with all that I got the following graph. The x-axis here is size of a crate's dependency graph (all crates involved). The y-axis is the ratio of "upstream code" to "local code", plotted on a logarithmic scale. That is, any data point greater than 1 means that you're generating more upstream code than local code.

This graph confirms to me, for example, that we have an exponential code growth problem. As you pick up more dependencies, the amount of code that just the final crate has to work with is growing exponentially. Way over half the crates on crates.io have over half their compile time to "blame" to upstream crates!


So basically what I wanted to get at is that rustc compile times are slow, and I believe the serious contributing factors are:

  • Generics are compiled with monomorphization, which generates more code than other strategies.
  • Generic Rust is idiomatic Rust, or in other words our own idioms are making our compile times worse
  • This problem gets worse the more and more Rust code you pull in, and idiomatic Rust projects pull in a lot of dependencies!

So with this mindset, I'm personally sort of opposed to discouraging the number one way to combat this problem, trait objects. I believe that this RFC would erase almost all usage of trait objects casually in the ecosystem, as it's more ergonomic to take a generic than it is to take a trait object.

I personally view this as the wrong default. I'd hypothesize that for 99% of code you don't actually need the performance benefits of 100% monomorphized code. Sure we need <[T]>::len to get inlined but do we really need an iterator over lines of a file to get 100% inlined?

An example of this is the tar crate which as an experiment I tried as hard as I could to write everything with trait objects instead of generics. What ends up happening is that you've still got generics and the benefits therein (lifetime inference, send/sync inference, etc), but the actual "meat" of the crate is all codegen'd in the crate itself, not affecting downstream compile times as much if the entire library were generic. To be clear I don't consider this crate idiomatic Rust, and I think in fact it's pretty advanced Rust to get that far in erasing generics.

On plausible scenario, though, is that instead of F: FnMut() we could switch everyone to taking &mut FnMut(). This is in fact what closures in Rust originally did (they were all virtually dispatched) and it also turns out that LLVM's devirtualization is quite good and covers almost all use cases where "performance matters"! If we consider the two APIs though:

fn foo<F: FnMut()>(f: F) { /* ... */ }
foo(|| ...);

// vs

fn foo(f: &mut FnMut()) { /* ... */ }
foo(&mut || ...);

One of these is clearly more ergonomic for the caller! Especially once we add &mut dyn FnMut it's both unergonomic for the caller and the library author!


All in all I don't really intend for this to be a rant, but rather my own personal rationale for why making trait objects worse in their current syntax to be a step in the wrong direction towards improving Rust's compile times. I think that Rust programmers lean on virtual dispatch far too rarely and this comes at a harm to Rust's compile times.

Now the problem of not using virtual dispatch goes much more deeper than the surface syntax. Types aren't object safe, they're tough to store in a struct, you deal with lifetimes quicker, etc. My point though is that we keep pushing into the world of hiding virtual dispatch as much as possible so even possible usages of trait objects today may shy away with the syntax of dyn required up front. And of course not adding dyn will not help Rust's compile times overnight, there'd need to be more work to make trait objects easier to use.

I think all I'm really getting at is that I'd personally like to see this as an explicitly mentioned drawback of the RFC. This won't make Rust's compile times 10x slower or anything, but it, in my mind, solidifies trait objects as a corner of the language only experts use. I'd personally prefer to try to push the needle in the other directoin (making trait objects easier to use), but I realize I'm very likely to be in the minority!

I do think that @nikomatsakis's thoughts about <dyn T> are promising but without a full design it's unclear if we can really lean on this :(

Member

alexcrichton commented Aug 17, 2017

I'm personally not necessarily against this RFC but not necessarily for this RFC. My opinion purely stems from compile times in Rust today, and let me elaborate!

Two of the headline sections of the motivation section in this RFC right now are:

  • favors a feature that is not more frequently used than its alternatives
  • favors a feature that ... is sometimes slower

And these are correct! It's pretty rare to see trait objects today and they are indeed factually slower than inlined methods in any possible microbenchmark. My thinking, though, is that this comes at a very real cost to compile times in the compiler.

The number one source of compile time slowness today is, I believe, due to monomorphization. This is because whenever you call a generic function we have to generate LLVM IR for that function call (each time it's called) and LLVM also has to optimize it and generate code for it. On small scales this doesn't matter too much but I'd like to talk about Rust code "in the large" for a moment.

I'd like to start out with some data I've collected to try to confirm this hypothesis. I wanted to analyze the top thousand crates on crates.io and evaluate how much code the compiler translates specifically for that one crate and specifically for upstream crates. That is, if you call a generic function in the standard library, like mpsc::channel(), that's "std's fault" whereas the main function of a crate is "your fault".

For each crate on crates.io I emitted the IR for the final crate. For each function in this final crate I classified each LLVM function as "from dependencies" or "local" depending on the debuginfo (looking at the filename). I then counted the total number of lines of LLVM IR to attribute to "from dependencies" and "local".

And with all that I got the following graph. The x-axis here is size of a crate's dependency graph (all crates involved). The y-axis is the ratio of "upstream code" to "local code", plotted on a logarithmic scale. That is, any data point greater than 1 means that you're generating more upstream code than local code.

This graph confirms to me, for example, that we have an exponential code growth problem. As you pick up more dependencies, the amount of code that just the final crate has to work with is growing exponentially. Way over half the crates on crates.io have over half their compile time to "blame" to upstream crates!


So basically what I wanted to get at is that rustc compile times are slow, and I believe the serious contributing factors are:

  • Generics are compiled with monomorphization, which generates more code than other strategies.
  • Generic Rust is idiomatic Rust, or in other words our own idioms are making our compile times worse
  • This problem gets worse the more and more Rust code you pull in, and idiomatic Rust projects pull in a lot of dependencies!

So with this mindset, I'm personally sort of opposed to discouraging the number one way to combat this problem, trait objects. I believe that this RFC would erase almost all usage of trait objects casually in the ecosystem, as it's more ergonomic to take a generic than it is to take a trait object.

I personally view this as the wrong default. I'd hypothesize that for 99% of code you don't actually need the performance benefits of 100% monomorphized code. Sure we need <[T]>::len to get inlined but do we really need an iterator over lines of a file to get 100% inlined?

An example of this is the tar crate which as an experiment I tried as hard as I could to write everything with trait objects instead of generics. What ends up happening is that you've still got generics and the benefits therein (lifetime inference, send/sync inference, etc), but the actual "meat" of the crate is all codegen'd in the crate itself, not affecting downstream compile times as much if the entire library were generic. To be clear I don't consider this crate idiomatic Rust, and I think in fact it's pretty advanced Rust to get that far in erasing generics.

On plausible scenario, though, is that instead of F: FnMut() we could switch everyone to taking &mut FnMut(). This is in fact what closures in Rust originally did (they were all virtually dispatched) and it also turns out that LLVM's devirtualization is quite good and covers almost all use cases where "performance matters"! If we consider the two APIs though:

fn foo<F: FnMut()>(f: F) { /* ... */ }
foo(|| ...);

// vs

fn foo(f: &mut FnMut()) { /* ... */ }
foo(&mut || ...);

One of these is clearly more ergonomic for the caller! Especially once we add &mut dyn FnMut it's both unergonomic for the caller and the library author!


All in all I don't really intend for this to be a rant, but rather my own personal rationale for why making trait objects worse in their current syntax to be a step in the wrong direction towards improving Rust's compile times. I think that Rust programmers lean on virtual dispatch far too rarely and this comes at a harm to Rust's compile times.

Now the problem of not using virtual dispatch goes much more deeper than the surface syntax. Types aren't object safe, they're tough to store in a struct, you deal with lifetimes quicker, etc. My point though is that we keep pushing into the world of hiding virtual dispatch as much as possible so even possible usages of trait objects today may shy away with the syntax of dyn required up front. And of course not adding dyn will not help Rust's compile times overnight, there'd need to be more work to make trait objects easier to use.

I think all I'm really getting at is that I'd personally like to see this as an explicitly mentioned drawback of the RFC. This won't make Rust's compile times 10x slower or anything, but it, in my mind, solidifies trait objects as a corner of the language only experts use. I'd personally prefer to try to push the needle in the other directoin (making trait objects easier to use), but I realize I'm very likely to be in the minority!

I do think that @nikomatsakis's thoughts about <dyn T> are promising but without a full design it's unclear if we can really lean on this :(

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Aug 18, 2017

Contributor

@alexcrichton that's an awesome comment!

Compile times are in fact my top one concern with the Rust compiler. But for me compile times are not important enough to justify making Rust a slower language. That being said, I do think that generics in public APIs of a library should be encouraged only where they are actually needed.

Contributor

est31 commented Aug 18, 2017

@alexcrichton that's an awesome comment!

Compile times are in fact my top one concern with the Rust compiler. But for me compile times are not important enough to justify making Rust a slower language. That being said, I do think that generics in public APIs of a library should be encouraged only where they are actually needed.

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Aug 18, 2017

Member

👍 I really like that this improves the "Trait is sometimes a trait and sometimes a type" situation. This way, Trait is always a trait, but impl Trait and dyn Trait are types.

Member

scottmcm commented Aug 18, 2017

👍 I really like that this improves the "Trait is sometimes a trait and sometimes a type" situation. This way, Trait is always a trait, but impl Trait and dyn Trait are types.

@withoutboats

This comment has been minimized.

Show comment
Hide comment
@withoutboats

withoutboats Aug 18, 2017

Contributor

@alexcrichton I agree that dynamic dispatch is unfairly maligned, and I don't think "making dynamic dispatch worse" is a motivation for this RFC (the compelling motivations to me are those to do with learnability).

I'd like in the future to make trait objects more ergonomic to work with, which I think has less to do with their type syntax and more to do with how object safety feels restrictive & how you have to box them to deal with them by value.

Contributor

withoutboats commented Aug 18, 2017

@alexcrichton I agree that dynamic dispatch is unfairly maligned, and I don't think "making dynamic dispatch worse" is a motivation for this RFC (the compelling motivations to me are those to do with learnability).

I'd like in the future to make trait objects more ergonomic to work with, which I think has less to do with their type syntax and more to do with how object safety feels restrictive & how you have to box them to deal with them by value.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Aug 18, 2017

Member

@alexcrichton

I believe that this RFC would erase almost all usage of trait objects casually in the ecosystem, as it's more ergonomic to take a generic than it is to take a trait object.

Can you explain why you think this RFC makes it more ergonomic to use generics than to use trait objects? fn foo(x: &impl Trait) { ... } and fn foo(x: &dyn Trait) { ... } look similarly ergonomic to me. Are you referring to the increased flexibility of generics due to Sized-ness?

Member

cramertj commented Aug 18, 2017

@alexcrichton

I believe that this RFC would erase almost all usage of trait objects casually in the ecosystem, as it's more ergonomic to take a generic than it is to take a trait object.

Can you explain why you think this RFC makes it more ergonomic to use generics than to use trait objects? fn foo(x: &impl Trait) { ... } and fn foo(x: &dyn Trait) { ... } look similarly ergonomic to me. Are you referring to the increased flexibility of generics due to Sized-ness?

@alexcrichton

This comment has been minimized.

Show comment
Hide comment
@alexcrichton

alexcrichton Aug 18, 2017

Member

@withoutboats a very good point! We talked a bit about this last night as well, and the learnability aspect really resonates with me as well. I think I've over-rotated on the "favors a feature that ... is sometimes slower" point in the motivation of this RFC too much.

I think you've got a very good point that the syntax may not be much of a deterrent here, but rather all of the papercuts with trait objects (object safety, caller sigils, etc). I think I just got a bit carried away with the "cost of monomorphization" semi-rant!

@cramertj I personally at least today see the trait object and generic syntax as "roughly equivalent" on the library-author side of things, for example:

fn foo<F: FnMut()>(f: F) { /* ... */ }
fn foo(f: &mut FnMut()) { /* ... */ }

In that sense I'd subjectively view the &mut dyn FnMut annotation to be less ergonomic than what we have today if you zero in on just this one feature. In terms of comparison with impl Trait though you have a good point! I don't think I've successfully migrated my thinking to impl Trait yet, so I'd be comparing:

fn foo<F: FnMut()>(f: F) { /* ... */ }
fn foo(f: &mut dyn FnMut()) { /* ... */ }

instead of

fn foo(f: impl FnMut()) { /* ... */ }
fn foo(f: &mut dyn FnMut()) { /* ... */ }

I agree that the latter (using impl FnMut()) is less "subjectively unergonomic" to me than in the former case!

Regardless though discussion with @withoutboats last night really clarified for me that the heart of this proposal is about the learnability aspect of dynamic dispatch today. That definitely makes sense to me, and attempting to pursue, in parallel, proposals to make dyn Trait easier to work with (working with object safety and such) makes sense to me.

Member

alexcrichton commented Aug 18, 2017

@withoutboats a very good point! We talked a bit about this last night as well, and the learnability aspect really resonates with me as well. I think I've over-rotated on the "favors a feature that ... is sometimes slower" point in the motivation of this RFC too much.

I think you've got a very good point that the syntax may not be much of a deterrent here, but rather all of the papercuts with trait objects (object safety, caller sigils, etc). I think I just got a bit carried away with the "cost of monomorphization" semi-rant!

@cramertj I personally at least today see the trait object and generic syntax as "roughly equivalent" on the library-author side of things, for example:

fn foo<F: FnMut()>(f: F) { /* ... */ }
fn foo(f: &mut FnMut()) { /* ... */ }

In that sense I'd subjectively view the &mut dyn FnMut annotation to be less ergonomic than what we have today if you zero in on just this one feature. In terms of comparison with impl Trait though you have a good point! I don't think I've successfully migrated my thinking to impl Trait yet, so I'd be comparing:

fn foo<F: FnMut()>(f: F) { /* ... */ }
fn foo(f: &mut dyn FnMut()) { /* ... */ }

instead of

fn foo(f: impl FnMut()) { /* ... */ }
fn foo(f: &mut dyn FnMut()) { /* ... */ }

I agree that the latter (using impl FnMut()) is less "subjectively unergonomic" to me than in the former case!

Regardless though discussion with @withoutboats last night really clarified for me that the heart of this proposal is about the learnability aspect of dynamic dispatch today. That definitely makes sense to me, and attempting to pursue, in parallel, proposals to make dyn Trait easier to work with (working with object safety and such) makes sense to me.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Aug 20, 2017

Has anyone considered using dyn like syntax for automagically type erased traits to provide easier trait objects? Dyn Trait could be some type erased transformation of Trait. Or is doing something like erased-serde automatically not possible? If this were possible, then &mut dyn Dyn Trait might be kinda annoying.

burdges commented Aug 20, 2017

Has anyone considered using dyn like syntax for automagically type erased traits to provide easier trait objects? Dyn Trait could be some type erased transformation of Trait. Or is doing something like erased-serde automatically not possible? If this were possible, then &mut dyn Dyn Trait might be kinda annoying.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Aug 20, 2017

Member

I'd use dyn Trait as a shorthand for dyn<T: Trait> T but maybe that syntax for dynamic existentials is a bit over the top.

Member

eddyb commented Aug 20, 2017

I'd use dyn Trait as a shorthand for dyn<T: Trait> T but maybe that syntax for dynamic existentials is a bit over the top.

@gnzlbg

This comment has been minimized.

Show comment
Hide comment
@gnzlbg

gnzlbg Aug 21, 2017

Contributor

@alexcrichton

Almost all compiler optimizations rely on inlining, but as you say, inlining destroys separate compilation. That is, monomorphization makes code fast and compile times exponentially slow, while trait objects make code slow and compile times fast (linear). Obviously, this is not black & white, and in many cases the opposite is true, but in general, I think we can agree that this holds.

What I do not agree with is that dynamic dispatch makes it a better default. IMO it is way way easier to make code that uses monomorphization everywhere compile fast (by caching, limiting inlining, outlining, ...) than to make code that uses dynamic dispatch run fast (whole-program link-time optimization, devirtualization, "just use ThinLTO and hope for the best", ...) .

So sure, monomorphization increases code-size, compile-times, etc. and this is a known problem in every programming language out there. Are we already doing all of the following?:

  • caching type-checking of generics in APIs?
  • compiling generic code in APIs to byte-code that can be quickly monomorphized?
  • caching monomorphizations when building many crates (so that only a single crate has to monomorphize, e.g., Vec<usize>) ?
  • not inlining generics in debug mode? (so that cached monomorphizations can be directly reused)
  • caching code generation for generics in debug mode, so that code generation only emits code for Vec<usize> once for the whole crate?
  • providing pre-monomorphized code in the standard library? (e.g. Vec<f32>, Vec<f64>, ... so that no user needs to monomorphize these types but just link against them, in e.g., debug mode).
  • allowing library authors to provide pre-monomorphized code?
  • hell, maybe even type-erasing generic code when possible? That is if a generic function can work on trait objects, monomorphizing it and generating code for the trait object version only once, and automatically wrapping non-trait-object arguments into trait objects, and unwrapping the results when necessary. This could slow code a lot, but it would break the exponential compile-times.

IMO, pursuing all of these approaches to speed up the compilation of generics by trading off performance is way easier than pursuing even the most trivial optimizations for trait objects.

OTOH if we switch to dynamic dispatch "by default", and performance becomes an issue, there isn't really a lot that we can do to improve on it. This is why I think that what you propose, that is, pursuing trait objects by default, has a way higher risk long term than what we currently have.

Contributor

gnzlbg commented Aug 21, 2017

@alexcrichton

Almost all compiler optimizations rely on inlining, but as you say, inlining destroys separate compilation. That is, monomorphization makes code fast and compile times exponentially slow, while trait objects make code slow and compile times fast (linear). Obviously, this is not black & white, and in many cases the opposite is true, but in general, I think we can agree that this holds.

What I do not agree with is that dynamic dispatch makes it a better default. IMO it is way way easier to make code that uses monomorphization everywhere compile fast (by caching, limiting inlining, outlining, ...) than to make code that uses dynamic dispatch run fast (whole-program link-time optimization, devirtualization, "just use ThinLTO and hope for the best", ...) .

So sure, monomorphization increases code-size, compile-times, etc. and this is a known problem in every programming language out there. Are we already doing all of the following?:

  • caching type-checking of generics in APIs?
  • compiling generic code in APIs to byte-code that can be quickly monomorphized?
  • caching monomorphizations when building many crates (so that only a single crate has to monomorphize, e.g., Vec<usize>) ?
  • not inlining generics in debug mode? (so that cached monomorphizations can be directly reused)
  • caching code generation for generics in debug mode, so that code generation only emits code for Vec<usize> once for the whole crate?
  • providing pre-monomorphized code in the standard library? (e.g. Vec<f32>, Vec<f64>, ... so that no user needs to monomorphize these types but just link against them, in e.g., debug mode).
  • allowing library authors to provide pre-monomorphized code?
  • hell, maybe even type-erasing generic code when possible? That is if a generic function can work on trait objects, monomorphizing it and generating code for the trait object version only once, and automatically wrapping non-trait-object arguments into trait objects, and unwrapping the results when necessary. This could slow code a lot, but it would break the exponential compile-times.

IMO, pursuing all of these approaches to speed up the compilation of generics by trading off performance is way easier than pursuing even the most trivial optimizations for trait objects.

OTOH if we switch to dynamic dispatch "by default", and performance becomes an issue, there isn't really a lot that we can do to improve on it. This is why I think that what you propose, that is, pursuing trait objects by default, has a way higher risk long term than what we currently have.

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Aug 21, 2017

Contributor

caching type-checking of generics in APIs?

Sure. Type-checking is only done once.

compiling generic code in APIs to byte-code that can be quickly monomorphized?

We have byte-code ("MIR"), but because of how people write it + the absence of MIR inlining, it takes some amount of time to monomorphize.

caching monomorphizations when building many crates (so that only a single crate has to monomorphize, e.g., Vec<usize>) ?

People don't tend to use Vec<usize> but rather Vec<Rc<MyPrivateType>>. Because Vec needs to call destructors, sharing the code there is not so trivial - maybe the implementation of Vec could "manually" do dispatching based on size/align.

caching code generation for generics in debug mode, so that code generation only emits code for Vec<usize> once for the whole crate?

The same type (e.g. literally Vec<usize>) will always be codegenned once per crate. However, people tend to use Vec<MyPrivateTypeN>, which requires 1 monomorphization/instance.

providing pre-monomorphized code in the standard library? (e.g. Vec<f32>, Vec<f64>, ... so that no user needs to monomorphize these types but just link against them, in e.g., debug mode).

No, but again complex types.

Contributor

arielb1 commented Aug 21, 2017

caching type-checking of generics in APIs?

Sure. Type-checking is only done once.

compiling generic code in APIs to byte-code that can be quickly monomorphized?

We have byte-code ("MIR"), but because of how people write it + the absence of MIR inlining, it takes some amount of time to monomorphize.

caching monomorphizations when building many crates (so that only a single crate has to monomorphize, e.g., Vec<usize>) ?

People don't tend to use Vec<usize> but rather Vec<Rc<MyPrivateType>>. Because Vec needs to call destructors, sharing the code there is not so trivial - maybe the implementation of Vec could "manually" do dispatching based on size/align.

caching code generation for generics in debug mode, so that code generation only emits code for Vec<usize> once for the whole crate?

The same type (e.g. literally Vec<usize>) will always be codegenned once per crate. However, people tend to use Vec<MyPrivateTypeN>, which requires 1 monomorphization/instance.

providing pre-monomorphized code in the standard library? (e.g. Vec<f32>, Vec<f64>, ... so that no user needs to monomorphize these types but just link against them, in e.g., debug mode).

No, but again complex types.

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Aug 21, 2017

Contributor

However, I think a main part of the "exponential" problem occurs because of "code at scale", e.g. hyper_tls::stream::MaybeHttpsStream takes a type parameter, so hyper_tls::stream::MaybeHttpsStream<tokio_core::net::tcp::TcpStream> has to be monomorphized in several child crates.

Contributor

arielb1 commented Aug 21, 2017

However, I think a main part of the "exponential" problem occurs because of "code at scale", e.g. hyper_tls::stream::MaybeHttpsStream takes a type parameter, so hyper_tls::stream::MaybeHttpsStream<tokio_core::net::tcp::TcpStream> has to be monomorphized in several child crates.

@gnzlbg

This comment has been minimized.

Show comment
Hide comment
@gnzlbg

gnzlbg Aug 21, 2017

Contributor

@arielb1 When I mentioned caching, I was referring to do so across all crates. E.g. if a crate A exposes a generic function foo and crate B exposes the type Bar, and then both crates C and D call foo<Bar> then the code for that should be ideally generated only once, such that if C comes first, then it waits for it, but afterwards when crate D asks, then the code is accessed from the cache and we can skip MIR, LLVM, ...

People don't tend to use Vec but rather Vec<Rc>

When private crate types are involved, then obviously this doesn't help, because the monomorphization / code generation happens already at crate granularity, and other crates cannot name private types, so they cannot ask for the same monomorphizations.

Do we have statistics for this? That is, statistics about the number of unique monomorphizations (asked once, required once) vs repeated monomorphizations (asked by many crates) when building large projects like servo? These numbers might be interesting.

A common compile-time optimization in C++ is to factor out the part of the code that does not need to know about the exact type into a base-class. But these are more "library-specific" optimizations to reduce compile-times, I was only talking about general optimizations that don't require users to change any code.

However, I think a main part of the "exponential" problem occurs because of "code at scale", e.g. hyper_tls::stream::MaybeHttpsStream takes a type parameter, so hyper_tls::stream::MaybeHttpsStream<tokio_core::net::tcp::TcpStream> has to be monomorphized in several child crates.

Sure, and if the types in the child crates are different, then that's the way it is (unless we start trying to type-erase generics in debug builds). But if all child crates instantiate it with the same types, right now, the code is monomorphized N times, instead of just once.

We have byte-code ("MIR"), but because of how people write it

What do you mean by "how people write it" ?

Contributor

gnzlbg commented Aug 21, 2017

@arielb1 When I mentioned caching, I was referring to do so across all crates. E.g. if a crate A exposes a generic function foo and crate B exposes the type Bar, and then both crates C and D call foo<Bar> then the code for that should be ideally generated only once, such that if C comes first, then it waits for it, but afterwards when crate D asks, then the code is accessed from the cache and we can skip MIR, LLVM, ...

People don't tend to use Vec but rather Vec<Rc>

When private crate types are involved, then obviously this doesn't help, because the monomorphization / code generation happens already at crate granularity, and other crates cannot name private types, so they cannot ask for the same monomorphizations.

Do we have statistics for this? That is, statistics about the number of unique monomorphizations (asked once, required once) vs repeated monomorphizations (asked by many crates) when building large projects like servo? These numbers might be interesting.

A common compile-time optimization in C++ is to factor out the part of the code that does not need to know about the exact type into a base-class. But these are more "library-specific" optimizations to reduce compile-times, I was only talking about general optimizations that don't require users to change any code.

However, I think a main part of the "exponential" problem occurs because of "code at scale", e.g. hyper_tls::stream::MaybeHttpsStream takes a type parameter, so hyper_tls::stream::MaybeHttpsStream<tokio_core::net::tcp::TcpStream> has to be monomorphized in several child crates.

Sure, and if the types in the child crates are different, then that's the way it is (unless we start trying to type-erase generics in debug builds). But if all child crates instantiate it with the same types, right now, the code is monomorphized N times, instead of just once.

We have byte-code ("MIR"), but because of how people write it

What do you mean by "how people write it" ?

@arielb1

This comment has been minimized.

Show comment
Hide comment
@arielb1

arielb1 Aug 21, 2017

Contributor

What do you mean by "how people write it" ?

Rust code has lots of calls to tiny helper functions like Deref::deref, trivial IntoIterator::into_iter, trivial accessors, etc., that mean that without inlining the control and data flow are really not evident from MIR. In C++ this is much less of a problem.

Contributor

arielb1 commented Aug 21, 2017

What do you mean by "how people write it" ?

Rust code has lots of calls to tiny helper functions like Deref::deref, trivial IntoIterator::into_iter, trivial accessors, etc., that mean that without inlining the control and data flow are really not evident from MIR. In C++ this is much less of a problem.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Aug 21, 2017

The same type (e.g. literally Vec) will always be codegenned once per crate.

I presume this means once per binary, not per crate in the binary.

A common compile-time optimization in C++ is to factor out the part of the code that does not need to know about the exact type into a base-class.

I'd think compiler optimizations could often do this under the hood by creating some form of light weight internal function call with a light weight internal trait object. An inlined monomorphized function foo becomes a simple, but not necessarily object safe, wrapper around an somewhat object safe core go_foo. The core would look less ergonomic, but users would never see it. Rust might already optimize this correctly :

    fn foo(&self, bar: Bar) ->Result<Foo,Error> {  // Not object safe
        let mut r: Result<Foo,Error>;
        fn go_foo(&self, bar: &Bar, foo: &mut Result<Foo,Error>) {  // Object safe
            ...
        }
        self.go_foo(&bar,&mut r)
        r
    }

Or maybe you should write :

        let mut r: Foo;
        fn go_foo(&self, bar: &Bar, foo: &mut Foo) -> Result<(),Error>{  // Object safe
            ...
        }
        self.go_foo(&bar,&mut r).map(|| r)

burdges commented Aug 21, 2017

The same type (e.g. literally Vec) will always be codegenned once per crate.

I presume this means once per binary, not per crate in the binary.

A common compile-time optimization in C++ is to factor out the part of the code that does not need to know about the exact type into a base-class.

I'd think compiler optimizations could often do this under the hood by creating some form of light weight internal function call with a light weight internal trait object. An inlined monomorphized function foo becomes a simple, but not necessarily object safe, wrapper around an somewhat object safe core go_foo. The core would look less ergonomic, but users would never see it. Rust might already optimize this correctly :

    fn foo(&self, bar: Bar) ->Result<Foo,Error> {  // Not object safe
        let mut r: Result<Foo,Error>;
        fn go_foo(&self, bar: &Bar, foo: &mut Result<Foo,Error>) {  // Object safe
            ...
        }
        self.go_foo(&bar,&mut r)
        r
    }

Or maybe you should write :

        let mut r: Foo;
        fn go_foo(&self, bar: &Bar, foo: &mut Foo) -> Result<(),Error>{  // Object safe
            ...
        }
        self.go_foo(&bar,&mut r).map(|| r)
@stevenblenkinsop

This comment has been minimized.

Show comment
Hide comment
@stevenblenkinsop

stevenblenkinsop Aug 31, 2017

@archer884
Dynamic dispatch isn't a "bad thing", and the intent of this proposal isn't to make it harder. You keep circling back to this, and it isn't an accurate characterization of the proposal. The point is to make dynamic dispatch more transparent, and make the language make more sense and thus easier to teach. The net positive is that the benefit of a clearer syntax outweighs the cost of having a three letter keyword.

stevenblenkinsop commented Aug 31, 2017

@archer884
Dynamic dispatch isn't a "bad thing", and the intent of this proposal isn't to make it harder. You keep circling back to this, and it isn't an accurate characterization of the proposal. The point is to make dynamic dispatch more transparent, and make the language make more sense and thus easier to teach. The net positive is that the benefit of a clearer syntax outweighs the cost of having a three letter keyword.

@archer884

This comment has been minimized.

Show comment
Hide comment
@archer884

archer884 Sep 1, 2017

@stevenblenkinsop I'm happy to agree to disagree with you. Just here to express my view of the RFC.

archer884 commented Sep 1, 2017

@stevenblenkinsop I'm happy to agree to disagree with you. Just here to express my view of the RFC.

@Ixrec

This comment has been minimized.

Show comment
Hide comment
@Ixrec

Ixrec Sep 6, 2017

Contributor

Updated the RFC to specify that bare trait syntax would not be removed completely in the next epoch, but instead become a deny-by-default lint, in accordance with the latest updates to the epochs RFC as summarized here.

Because that RFC does allow us to "leverage the corresponding lint setting to produce error messages as if the feature were removed entirely," all of the benefits of this RFC remain intact.

Contributor

Ixrec commented Sep 6, 2017

Updated the RFC to specify that bare trait syntax would not be removed completely in the next epoch, but instead become a deny-by-default lint, in accordance with the latest updates to the epochs RFC as summarized here.

Because that RFC does allow us to "leverage the corresponding lint setting to produce error messages as if the feature were removed entirely," all of the benefits of this RFC remain intact.

@cramertj

This comment has been minimized.

Show comment
Hide comment
@cramertj

cramertj Sep 6, 2017

Member

I've set up a video meeting to discuss impl Trait syntax (details here). I thought I would cross-post to this thread since there's a lot of shared interest and concerns surrounding dyn Trait and impl Trait syntax.

Member

cramertj commented Sep 6, 2017

I've set up a video meeting to discuss impl Trait syntax (details here). I thought I would cross-post to this thread since there's a lot of shared interest and concerns surrounding dyn Trait and impl Trait syntax.

@rfcbot

This comment has been minimized.

Show comment
Hide comment
@rfcbot

rfcbot Sep 7, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

rfcbot commented Sep 7, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

@bstrie

This comment has been minimized.

Show comment
Hide comment
@bstrie

bstrie Sep 14, 2017

Contributor

No bringing back sigils, please. :)

dyn is fine as a keyword, and "dynamic" is better jargon than "virtual".

I'm still not sure whether this RFC is worth accepting. For all you people out there hoping that this will turn impl Trait into Trait, the prevailing sentiment from this thread seems to be that it's good to keep traits and types syntactically separate, which will preclude bare Trait from ever happening (presupposing a breaking change to get rid of bare Trait for trait objects, of course).

Given the nature of this change, I also question whether it isn't premature to accept this given that the epochs RFC has yet to be accepted.

There are plenty of people who look at the bevy of RFCs being accepted recently, and the dozen more standing by to be accepted, and wonder if the impending impl period isn't leading to premature decisions. It erodes trust in our decision-making processes. Let's make sure we're not making such changes frivolously.

Contributor

bstrie commented Sep 14, 2017

No bringing back sigils, please. :)

dyn is fine as a keyword, and "dynamic" is better jargon than "virtual".

I'm still not sure whether this RFC is worth accepting. For all you people out there hoping that this will turn impl Trait into Trait, the prevailing sentiment from this thread seems to be that it's good to keep traits and types syntactically separate, which will preclude bare Trait from ever happening (presupposing a breaking change to get rid of bare Trait for trait objects, of course).

Given the nature of this change, I also question whether it isn't premature to accept this given that the epochs RFC has yet to be accepted.

There are plenty of people who look at the bevy of RFCs being accepted recently, and the dozen more standing by to be accepted, and wonder if the impending impl period isn't leading to premature decisions. It erodes trust in our decision-making processes. Let's make sure we're not making such changes frivolously.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Sep 14, 2017

Contributor

For all you people out there hoping that this will turn impl Trait into Trait, the prevailing sentiment from this thread seems to be that it's good to keep traits and types syntactically separate, which will preclude bare Trait from ever happening

I in fact hope that impl Trait remains that way. I don't think bare traits should have any meaning as types, they could otherwise be confused with structs/enums too easily. Both impl and dyn do some stuff under the hood and its good that this is observable.

Contributor

est31 commented Sep 14, 2017

For all you people out there hoping that this will turn impl Trait into Trait, the prevailing sentiment from this thread seems to be that it's good to keep traits and types syntactically separate, which will preclude bare Trait from ever happening

I in fact hope that impl Trait remains that way. I don't think bare traits should have any meaning as types, they could otherwise be confused with structs/enums too easily. Both impl and dyn do some stuff under the hood and its good that this is observable.

@tmzt

This comment has been minimized.

Show comment
Hide comment
@tmzt

tmzt Sep 14, 2017

I agree with @est31 on the last part, but bare trait syntax could repurposed for cases like

impl Trait1 for Trait2 { ... }

Which could be syntactic sugar for:

impl<T: Trait2> Trait1 for T { ... }

This would eliminate confusion about what the former is really doing and why it's giving such confusing errors for non object-safe traits.

Appologies if syntax is wring, on mobile.

tmzt commented Sep 14, 2017

I agree with @est31 on the last part, but bare trait syntax could repurposed for cases like

impl Trait1 for Trait2 { ... }

Which could be syntactic sugar for:

impl<T: Trait2> Trait1 for T { ... }

This would eliminate confusion about what the former is really doing and why it's giving such confusing errors for non object-safe traits.

Appologies if syntax is wring, on mobile.

@Visic

This comment has been minimized.

Show comment
Hide comment
@Visic

Visic Sep 14, 2017

@tmzt
That would mean going from the current syntax for implementing a trait for trait objects of another trait type, to the same exact syntax for implementing a trait for concrete types that implement some trait, which would be extremely confusing.

In addition, what I like best about @est31's proposal, is that how the code reads, is a lot more accurate to what is going on.

impl<T: Trait2> Trait1 for T { ... } clearly says "Implement Trait1 for any type which implements Trait2"
impl Trait1 for dyn Trait2 { ... } clearly says "Implement Trait1 for any trait object of type Trait2"
impl Trait1 for Trait2 { ... } doesn't really explain what is happening.

Visic commented Sep 14, 2017

@tmzt
That would mean going from the current syntax for implementing a trait for trait objects of another trait type, to the same exact syntax for implementing a trait for concrete types that implement some trait, which would be extremely confusing.

In addition, what I like best about @est31's proposal, is that how the code reads, is a lot more accurate to what is going on.

impl<T: Trait2> Trait1 for T { ... } clearly says "Implement Trait1 for any type which implements Trait2"
impl Trait1 for dyn Trait2 { ... } clearly says "Implement Trait1 for any trait object of type Trait2"
impl Trait1 for Trait2 { ... } doesn't really explain what is happening.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Sep 14, 2017

impl Trait1 for impl Trait2 { ... } sounds obnoxious too, but impl Trait1 for any Trait2 { ... } might work.

We should squander good distinctive syntaxes duplicating existing functionality because they create confusion and we've interesting things like inherent impls for traits that do not exist, well maybe impl<T: Foo> T { .. } eventually.

burdges commented Sep 14, 2017

impl Trait1 for impl Trait2 { ... } sounds obnoxious too, but impl Trait1 for any Trait2 { ... } might work.

We should squander good distinctive syntaxes duplicating existing functionality because they create confusion and we've interesting things like inherent impls for traits that do not exist, well maybe impl<T: Foo> T { .. } eventually.

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Sep 15, 2017

Member

Since impl Trait in argument position is elided generics:

impl Trait1 for impl Trait2 { ... }

And then just use Self to refer to it?

Member

scottmcm commented Sep 15, 2017

Since impl Trait in argument position is elided generics:

impl Trait1 for impl Trait2 { ... }

And then just use Self to refer to it?

@mark-i-m

This comment has been minimized.

Show comment
Hide comment
@mark-i-m

mark-i-m Sep 15, 2017

Contributor

I'm strongly opposed to impl Trait1 for impl Trait2. The first impl means something different from the second one. That's just plain weird, and not totally obvious to beginners.

Contributor

mark-i-m commented Sep 15, 2017

I'm strongly opposed to impl Trait1 for impl Trait2. The first impl means something different from the second one. That's just plain weird, and not totally obvious to beginners.

@RalfJung

This comment has been minimized.

Show comment
Hide comment
@RalfJung

RalfJung Sep 15, 2017

Member

Re:

Finally, we could repurpose bare trait syntax for something other than trait objects. It's been frequently suggested in the past that impl Trait would be a far better candidate for bare trait syntax than trait objects.

Some time ago, @aturon wrote

This is in the context of the general regret7 over elided lifetimes in type constructors having no marker that they occur (making it hard to know when borrowing is happening), which has been discussed throughout this thread.

Here’s the thing: we can achieve the same goal by using reversed defaults! That is, the general assumption can be that impl Trait allows for borrowing according to the usual elision rules, making impl much like & when scanning for borrowing. When elision isn’t allowed, or when you want to override these rules, you can impose lifetime bounds.

If we ever have bare trait names used as types mean impl Trait, that approach will no longer work. However, this RFC doesn't propose that, so I guess this will only be a problem if someone ever suggests (in a future epoch) to allow bare traits for impl Trait.

Member

RalfJung commented Sep 15, 2017

Re:

Finally, we could repurpose bare trait syntax for something other than trait objects. It's been frequently suggested in the past that impl Trait would be a far better candidate for bare trait syntax than trait objects.

Some time ago, @aturon wrote

This is in the context of the general regret7 over elided lifetimes in type constructors having no marker that they occur (making it hard to know when borrowing is happening), which has been discussed throughout this thread.

Here’s the thing: we can achieve the same goal by using reversed defaults! That is, the general assumption can be that impl Trait allows for borrowing according to the usual elision rules, making impl much like & when scanning for borrowing. When elision isn’t allowed, or when you want to override these rules, you can impose lifetime bounds.

If we ever have bare trait names used as types mean impl Trait, that approach will no longer work. However, this RFC doesn't propose that, so I guess this will only be a problem if someone ever suggests (in a future epoch) to allow bare traits for impl Trait.

@WaDelma

This comment has been minimized.

Show comment
Hide comment
@WaDelma

WaDelma Sep 17, 2017

Would using trait Send be untenable syntax for this? Only thing that makes me hesitate with it is using trait Trait in examples.

Otherwise I prefer dyn for it's shortness as I think that we should avoid making dynamic dispatch that much more annoying to use (Box<dyn Send> vs Box<trait Send> vs Box<virtual Send>). I also don't think that any proposed non-abbreviated syntaxes are that much clearler in meaning.

WaDelma commented Sep 17, 2017

Would using trait Send be untenable syntax for this? Only thing that makes me hesitate with it is using trait Trait in examples.

Otherwise I prefer dyn for it's shortness as I think that we should avoid making dynamic dispatch that much more annoying to use (Box<dyn Send> vs Box<trait Send> vs Box<virtual Send>). I also don't think that any proposed non-abbreviated syntaxes are that much clearler in meaning.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Sep 17, 2017

Contributor

@WaDelma soon, trait objects won't have to be boxed in many situations (unsized rvalues RFC).

Contributor

est31 commented Sep 17, 2017

@WaDelma soon, trait objects won't have to be boxed in many situations (unsized rvalues RFC).

@rfcbot

This comment has been minimized.

Show comment
Hide comment
@rfcbot

rfcbot Sep 17, 2017

The final comment period is now complete.

rfcbot commented Sep 17, 2017

The final comment period is now complete.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Sep 17, 2017

Member

This RFC has been merged! For those who haven't followed the thread closely, the main change since the RFC was opened was to relax the rollout story, employing a lint for bare trait syntax, and only raising that lint to deny-by-default in the next epoch. This is as per the finalized epoch policy.

With respect to bikeshedding on the dyn keyword: there's yet to be an alternative contender strong enough to be preferred over dyn, though of course this is something we can continue pondering until stabilization. Probably the biggest issue with dyn to emerge is the disconnect from our "trait object" terminology; it's unclear how to remove that downside without losing the benefits of dyn, though obj perhaps comes closest.

In any case, discussion will now continue on the dedicated tracking issue.

Thanks all for the great discussion, and @Ixrec for writing the RFC!

Member

aturon commented Sep 17, 2017

This RFC has been merged! For those who haven't followed the thread closely, the main change since the RFC was opened was to relax the rollout story, employing a lint for bare trait syntax, and only raising that lint to deny-by-default in the next epoch. This is as per the finalized epoch policy.

With respect to bikeshedding on the dyn keyword: there's yet to be an alternative contender strong enough to be preferred over dyn, though of course this is something we can continue pondering until stabilization. Probably the biggest issue with dyn to emerge is the disconnect from our "trait object" terminology; it's unclear how to remove that downside without losing the benefits of dyn, though obj perhaps comes closest.

In any case, discussion will now continue on the dedicated tracking issue.

Thanks all for the great discussion, and @Ixrec for writing the RFC!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment