-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Generic details 9: default impl #1034
Conversation
docs/design/generics/details.md
Outdated
// As if followed by *internal* impl of Equatable: | ||
// impl as Equatable { | ||
// fn Equals[me: Self](rhs: Self) -> bool { | ||
// return me.Hash() == rhs.Hash(); | ||
// } | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it actually matter if the impl as Equatable
is internal? If both of the prerequisite conditions are met, the Hashable
interface contains all members of the Equatable
interface, so I'd expect Song
to have the same interface here regardless of whether we have an internal or external implementation of Equatable
.
If I'm not missing something, I think this rule would be equivalent: "A default impl
is always external. Note: Its interface will be available in the type if the original interface is implemented internally and extends the type for which a default impl
is provided."
I don't necessarily think that alternative presentation is any better; I think the rules are clear either way, and we want to spell out the consequences either way. I'm mostly just checking that there's not some subtlety here that I've overlooked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think your approach makes sense. I've updated the text, though I'm not super happy about how the wording came out. I might need to establish some terminology to talk about the two different interfaces involved.
external impl Song as PartialOrder { ... } | ||
``` | ||
|
||
You can achieve a similar effect as a default impl by using a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can achieve a similar effect as a default impl by using a | |
You can achieve a similar effect to a default impl by using a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is a US/UK difference? I checked with my wife and she'd say "as" in this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mm, could be. I know that the "different from" versus "different than"/"different to" is a UK/US thing ("different from" is the only one of those that's correct in formal British English), so a similar divergence for "similar to" would make sense. To my reading, "as" binds to "you" not to "similar", as in "you can travel a similar distance as a pedestrian ..." which makes this construction a little garden-pathy for me. Maybe a different word order would read well to both of us:
You can achieve a similar effect as a default impl by using a | |
You can achieve an effect similar to a default impl by using a |
The difference between the two approaches is the prioritization of the resulting | ||
implementations. The default impl approach results in a type structure of | ||
`impl Song as PartialOrder`, which has a higher priority than the blanket impl's | ||
type structure of `impl ? as PartialOrder`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see an example of what happens for a default impl
if it's generated from a parameterized impl
. I think there are two things to demonstrate here:
- That we still get a type structure that's more precise than a blanket impl, but one that's still parameterized.
- What happens if the default
impl
doesn't use all of the parameters of the outer interface (plusSelf
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think #2 is just a separate issue with parameterized interfaces independent of parameterization of the impl. I have added text addressing those points separately.
[associated constants](https://doc.rust-lang.org/reference/items/associated-items.html#associated-constants-examples). | ||
Rust has found them valuable. | ||
|
||
#### Default implementation of required interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The examples here are all impl as
and extends
. For symmetrizing, I think we would also want to support the case where the self type of the default impl
differs from the Self
type of the enclosing interface
(impl T as ComparableWith(Self)
). It'd be useful to include an example of that if we intend to support it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to include that, but I left it for a follow up since I realized I have not yet described that feature for interface requirements (though it is intended and a straightforward generalization).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've adding it since we have decided that is really part of this feature.
In addition, providing a default implementation of a required interface directly | ||
in the interface definition rather than in a separate blanket `impl` is expected | ||
to help make Carbon code | ||
[easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write), | ||
since that puts related code together and avoids repeating information relating | ||
those two interfaces together. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. What happens if you want to define some part of a default impl out-of-line? (Or if you have to, in order to break a cycle?) I guess we don't have a syntax for defining a method of a default impl, or even for naming one. Maybe that's a problem to solve in whatever proposal introduces the top-down ordering rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made that an alternative.
- Requiring both interfaces to be defined in the same library addresses | ||
incoherence concerns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see a bit more explanation of this point; given that a default impl
is effectively rewritten to something that the developer could have written themselves, it's not clear to me why there would be any coherence concerns if we allow an interface
in one library to provide a default impl
for an interface in another library.
I can think of cases where such permission might be useful. For example, if the Carbon prelude provides only AddableWith
, I could define my own SymmetricAddableWith
that extends AddableWith
and also provides a default impl
for the reversed form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've written more to explain this point. You are correct that there isn't technically a coherence concern, except that it would be more common for the orphan rule to prevent you from implementing SymmetricAddableWith
. I could remove this restriction with that caveat.
Co-authored-by: Richard Smith <richard@metafoo.co.uk>
external impl bool as WithParam(bool) { } | ||
// Triggers implementation of `bool as NoParam` | ||
|
||
external impl bool as WithParam(i32) { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if I write:
external impl i32 as WithParam(T:! Type) { }
? My guess is that this would be equivalent to:
external impl [T:! Type] i32 as WithParam(T) { }
... and would generate:
external impl [T:! Type] i32 as NoParam { }
... which would be invalid because T
is not deducible, and more generally, all wildcard impl
s of WithParam
would be disallowed.
Similarly for Self
:
interface Foo(T:! Type) {
impl T as NoParam { }
}
external impl i32 as Foo(i64) { }
// Injected: `external impl i64 as NoParam { };`
external impl (T:! Type) as Foo(i8) { }
// Injected: `external impl [T:! Type] i8 as NoParam { };`
// Rejected because that `impl` is invalid.
... and more generally, blanket impl
s of Foo
would be disallowed.
Examples of that would be useful, though I'm not sure if that belongs here or down below where you talk about parameterized implementations of interfaces with default impls.
Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Withdrawing this proposal. Consensus is this has too much overlap with blanket impls, and doesn't fully fix the priority problem that they have. |
When an interface requires another interface, using
impl as
orextends
, this proposal allows a default implementation to be provided inline, as in:The reason to use this instead of blanket impls is how they get prioritized differently.