Closed alrz closed 7 years ago
I'm not sure that I see how this works. #258 relies on the concrete class actually implementing the interface so the CLR will expect the slots for the implementing methods which it then wires up to the default implementations in the absence of an override provided by the class. Without that there are no slots to wire up or to call. The instance of A
still can't be cast to an IHasA
.
@HaloFour Yes, to cast an object to an interface which we provided the implementation afterwards the vtable shall be handled otherwise. I think this is something that is required to implement traits. But you can freely use it with generic constraints e.g.
void F<T>(T obj) where T : IHasM { }
F(new A());
Then we can dispatch the right method from constrained T
.
I'm not sure I see how you can "handle" slots in a vtable that don't exist. And being able to state that any given type now is a given interface seems quite messy and could have wide ramifications on existing code. If I had an interface check buried somewhere deep in a library and all of a sudden some unexpected classes started to pass that check it could cause unexpected results.
@HaloFour These are known as trait objects in Rust, when you define a function like void F(IHasM obj)
the object in question, obj
in this case, keeps a pointer to the concrete type provided vtable, hence you can pass any object to this funciton. while Rust does this for all trait objects, C# can only consider it for trait
types and disallow it for interfaces, for example,
class A { public void M() { } }
trait THasM { void M(); }
interface IHasM { void M(); }
implement IHasM for A { }
implement THasM for A { }
void F1(IHasM arg) { }
void F2(THasM arg) { }
void F3<T>(T arg) where T : IHasM { }
void F4<T>(T arg) where T : THasM { }
F1(new A()); // error as usual
F2(new A()); // ok, vtable pointer
F3(new A()); // ok, virtual extension methods
F4(new A()); // ok, same as above
@alrz I've not seen much discussion regarding the actual implementation of traits in C# but I don't think it would be comparable to Rust. I'd imagine that they'd be more like traits in Scala: interfaces where some of the methods provide default implementations, and that types are then required to extend directly. You'd need those method slots in the vtable to exist in order to be able to dispatch them, and classes that don't adopt that trait directly would not have those slots. An external mechanism would not be able to modify the vtable of an instance, especially of a type outside of that assembly. You'd also have generic type constraints enforced by the CLR which would definitely forbid all of F2
through F4
.
I just don't see it. Rust doesn't fit on the CLR.
@HaloFour Just like #258 this needs CLR support of course, but if F2
case doesn't fit on the CLR we'd be ok with generic constraints. I'm not saying that this should be done exactly the Rust way, but C# can adopt some part of it to cover some use cases in the language. I've seen much discussion regarding traits (not much about implementation though) but people want them to do zero-cost abstraction over primitives for example (as I said there are a lot of proposals regarding this matter) and they want get Haskell out of it but I think Rust approach is most applicable to C#. Closest proposal that I find was #3357 which doesn't add up much IMO.
@alrz That it does, but that's a much more limited change to the CLR keeping to the existing type structure and vtable mechanics. The only difference there being that if an interface defines a member that a concrete class does not implement but there is a known default implementation for the interface that the CLR will automatically assign the default implementation to the existing vtable slot rather than failing to load the type. Your proposal, however, is a very aggressive change involving adding a completely new form of typing (traits) as a first-class citizen along-side interfaces and basically making vtables a free-for-all.
My opinion is that I don't like the extent to which this proposal goes and I don't like that any type anywhere could have it's core contracts amended by an external actor. I don't think that someone should be able to arbitrarily fake that System.Int32
implements alrz.IFoo
. Seems ripe for abuse and could cause bizarre subtle effects if isinst
suddenly reports unexpected types as being some interface.
@HaloFour That's not where I'm going with this, I just mentioned traits to make my point and clear up the differences, as I said this has nothing or a little to do with traits but the syntax is flexible enough to be reused for trait implementations as well. You can think of implement
as a group of extension methods for a type, as an entity, so it just makes extension methods more coherent and you don't need to rely on the minimum level of coherence of SomethingExtensions
and we would take advantage of duck typing along the way. I'm utilizing virtual extension methods on a higher level, that's it. I'm thinking that isinst
wouldn't behave otherwise (at least not for interfaces), however, "any type anywhere could have it's core contracts amended by an external actor" this is true for traits and that's why they're useful, there’s a restriction on implementing traits though, either the trait or the type you’re implementing it for must be defined in the same assembly. I don't see why it would be problematic as it's not in the other languages.
@alrz Whatever it's called I'd think that the CLR mods would remain the same. You couldn't achieve F2
without some kind of new typing mechanism. You couldn't achieve F3
without the CLR officially supporting duck typing, which is the only way you'd satisfy the generic type constraint, and I don't see how that would (or should) be possible without affecting how the isinst
opcode behaves. You couldn't achieve F4
without expanding generic type constraints over this new typing mechanism.
Rust's type system is trait-based, the CLR is not. Rust owns it's type system outright, C# does not. I just don't see such radical changes to the CLR happening rather than a more iterative approach to support traits/mixins as done by other languages more similar to C# where a class must directly adopt the trait.
@HaloFour That restriction would apply to interfaces too. I think it addresses your concerns regarding implementation and usage, but it largely depends on #258 and what it adds to the CLR.
Even without interfaces, I think extension
syntax is a good alternative for defining extension members.
internal extension Random {
public bool NextBool() {
return this.Next(2) == 0;
}
}
// instead of
internal static class RandomExtensions {
public static bool NextBool(this Random self) {
return self.Next(2) == 0;
}
}
This'd also work for extension properties, operators etc. I'll update the opening post to use this syntax.
@alrz
Maybe, but we already have a syntax for extension methods. Also, if written like an instance method it might be even less clear that the this
argument can be null
. Not that this is particularly obvious now.
@HaloFour Currently, I don't know how extension properties would look like, because there is no way to define this T self
on properties! Besides, with this we get more coherent extension members around types. And as for your argument about nulls, I think we are making progress to not fall into that!
@alrz
Yes, we already have a proposal covering extension properties (and other things): #112 That's not related to some kind of "extension implementation" though so it doesn't seem particularly related to this proposal.
And as for your argument about nulls, I think we are making progress to not fall into that!
Sometimes this is quite intentional, but indeed #227 would at least be able to warn when it's not. Either way since extension methods are static and not invoked via callvirt
they don't get the built-in null
check. The CLR doesn't require that at all, either. Technically C# could use call
to invoke non-virtual instance methods and the this
argument would be null
legally, although the C# spec says that it won't.
@HaloFour Yes it's not particularly related but I wanted a syntax to cover both cases and be consistent with C# overall (implement
is not). Also, from SO:
this
can never benull
, unless the method was called using a call instruction in hand-written IL.
So there is no CLR enforcement. Technically, you should check this
for null
. I'm just sayin.
@alrz
So there is no CLR enforcement. Technically, you should check
this
fornul
l. I'm just sayin.
Technically, yes. You never know what language might be calling your assembly. And #227 will do nothing to help that. :smile:
We are now taking language feature discussion in other repositories:
Features that are under active design or development, or which are "championed" by someone on the language design team, have already been moved either as issues or as checked-in design documents. For example, the proposal in this repo "Proposal: Partial interface implementation a.k.a. Traits" (issue 16139 and a few other issues that request the same thing) are now tracked by the language team at issue 52 in https://github.com/dotnet/csharplang/issues, and there is a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md and further discussion at issue 288 in https://github.com/dotnet/csharplang/issues. Prototyping of the compiler portion of language features is still tracked here; see, for example, https://github.com/dotnet/roslyn/tree/features/DefaultInterfaceImplementation and issue 17952.
In order to facilitate that transition, we have started closing language design discussions from the roslyn repo with a note briefly explaining why. When we are aware of an existing discussion for the feature already in the new repo, we are adding a link to that. But we're not adding new issues to the new repos for existing discussions in this repo that the language design team does not currently envision taking on. Our intent is to eventually close the language design issues in the Roslyn repo and encourage discussion in one of the new repos instead.
Our intent is not to shut down discussion on language design - you can still continue discussion on the closed issues if you want - but rather we would like to encourage people to move discussion to where we are more likely to be paying attention (the new repo), or to abandon discussions that are no longer of interest to you.
If you happen to notice that one of the closed issues has a relevant issue in the new repo, and we have not added a link to the new issue, we would appreciate you providing a link from the old to the new discussion. That way people who are still interested in the discussion can start paying attention to the new issue.
Also, we'd welcome any ideas you might have on how we could better manage the transition. Comments and discussion about closing and/or moving issues should be directed to https://github.com/dotnet/roslyn/issues/18002. Comments and discussion about this issue can take place here or on an issue in the relevant repo.
This particular feature request would be satisfied by a combination of type classes (https://github.com/dotnet/csharplang/issues/110) and default interface methods (https://github.com/dotnet/csharplang/issues/52), both of which are under consideration for C#.
Consider this two interfaces.
So every class that implements
Iterator
is effectively anIterable
. To establish this relation, one might think of default methods:But this doesn't help because you can't say if the implementor is an
Iterator
. We might define the default method as generic constrained to anIterator
.But this design is somehow flawed (you might want more default generic implementations of the same method with different constraints) and limiting (you don't always have access to
Iterable
declaration).So you decide to declare an extension method for every
Iterator
.And now you didn't establish any relation to
Iterable
interface at all. You know that it does have a method namedGetIterator
(duck typing) but the compiler doesn't.So, here's my solution: define an implementation for every type that is an
Iterator
.Q: OK, What if we have other constraints as well? A: then you might define it as a generic implementation,
This is basically Rust's syntax for implementations but as an alternative you might suggest,
which is closer to Swift's extensions.
And how this would work? Via virtual extension methods (#258). The compiler generates a static class for extension methods on the target type (or on the constrained generic type).
Note that it is possible to add members without any interface in question, e.g.
It would be nice to be able to name the generated static class, then it's possible to refactor existing classes like
Enumerable
(using an analyzer, perhaps) in this manner without breaking existing code, otherwise, extension methods would be accessible via the target type itself, e.g.C.ExtensionMethodForC
.This can neatly cover the #7844 use case. Suppose we have two classes with no relation to each other.
As discussed in the aforementioned issue, this kind of duck typing can be inferred by the compiler but it cannot be done without a high risk of breaking existing code.
There’s an important restriction for implementations, either the interface or the type you’re implementing it for must be defined in the same assembly.
PS: (1) There are numerous open proposals regarding template/structural types, extension classes, etc, but none of them are directly related to #258 so I decided to open another one. (2) This is not about traits and zero-cost abstraction, but same syntax can be used for implementing traits for arbitrary types.