[Proposal] Implicit interfaces / Structural interface system #3260
Replies: 37 comments
-
Something like this is being considered, see #1711 on the latest proposal. |
Beta Was this translation helpful? Give feedback.
-
@svick Thanks for the link. I looked beforehand but must have missed it. Are there any plans for 9.0/10.0? This feature would make many things so much easier and better imho. |
Beta Was this translation helpful? Give feedback.
-
The name for this is usually duck typing. I would suggest renaming the issue to "Allow Duck Typed interface implementations" or something similar. This will definitely not happen in 9, but the team wants to start looking at it for 10, although there's no guarantees it will happen. That's about a year and half down the line. |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt In general I agree. The problem is that duck typing is mostly used for dynamic type systems which check type constraints at runtime. But I would like a compile-time type matching that is also usable with generics and so on. It's more like a structural type system which is limited to interfaces and is not usable for classes or structs. This will also remove some pitfalls with structural type systems as two classes that have the same properties by accident can not be treated as the same type. But a class which matches two interfaces which are structural equivalent will match both of that interfaces. But an interface is not a thing so it is not "class A is interface A and B" but it is "class A is compatible to the interface A and B" which is legit. |
Beta Was this translation helpful? Give feedback.
-
@Pyrdacor |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt The current C# interfaces may be real. But they can't be fully used as real interfaces. They are currently more of a constraint for classes and not much more. But I will think about it and rename the topic accordingly if I have a good name for it. |
Beta Was this translation helpful? Give feedback.
-
I think this is a really important feature. Many libraries that are written in C# could be designed in a much more flexible way and libraries could work much better together. Today most of the time if you have to swap a dependency you also have to create the interface to it anew. This change could lead to more compatible and flexible software in general and may be a key factor in using C# over other similar languages. |
Beta Was this translation helpful? Give feedback.
-
I called the topic structural interface system now as it is a structural type system for interfaces only. |
Beta Was this translation helpful? Give feedback.
-
Members of the team would agree, which is why it's a championed proposal with a series of discussions regarding implementation possibilities on this repo. It's not a simple proposal. C# is limited to functioning within the CLR type system which has no concept of this, and all purely language implementation options are either leaky abstractions or incur overhead. Given that your proposal is really just conceptual and doesn't dive into any of the implementation details that might differ it from type classes, shapes or "extension interfaces" I'd consider this a dupe. Perhaps you can expand on how exactly this proposal could be implemented? |
Beta Was this translation helpful? Give feedback.
-
@HaloFour I don't see the necessity of changing the CLR at all. This all should be handled by the compiler and (maybe) language features. The interfaces would still be of the same type. Even the current hierarchies like "class A implements interface B" still won't be touched. They remain valid and useful as they are. The only things that must be added are:
If it is necessary (I'm not deep enough into CLR etc) to have a specific type that implements the interface when some value is passed in as the interface, the compiler could just generate an object on the fly with a generated class (note this all happens at compile-time) that implements the interface and copies getter/setter/method implementations over from the passed object. Shouldn't hopefully be much of an issue. In worst case the compiler has to generate a class which extends the type of the passed object and implements the interface. Example: interface IPoint
{
int X { get; }
int Y { get; }
}
class Point // not implements IPoint explicitly
{
public int X => 0; // demonstration purpose
public int Y => 0; // demonstration purpose
}
void foo(IPoint point)
{
Console.WriteLine($"x: {point.X}, y: {point.Y}");
}
foo(new Point()); // this is not possible at the moment To make the last line possible the compiler could only create a temporary class like this: class CompilerGeneratedPoint : Point, IPoint
{
// content is equal to that of Point, compiler only adds IPoint as interface
}
// the call to foo becomes now
foo (new CompilerGeneratedPoint()); Now only the object of type Another (maybe better) approach would be that the compiler adds the |
Beta Was this translation helpful? Give feedback.
-
Which generates extra garbage, incurs double overhead on every call and will not support structs properly due to having to copy the original struct into the proxy object. The existing discussions on this subject touch on these issues and offer ways to resolve this both through only language changes as well as potentially through supporting CLR changes. |
Beta Was this translation helpful? Give feedback.
-
How so? The type is just replaced by the compiler once. There is no overhead at runtime. Fair point on structs though. But why on earth they can't just implement interfaces is still a mystery for me. I think this is a general issue like with |
Beta Was this translation helpful? Give feedback.
-
There is the addition of the virtual dispatch through the interface member on the generated proxy, on top of the actual method call to the target instance. Not to mention the cost of creating the proxy on every invocation to the method accepting the interface. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Maybe you got me wrong. The type could be replaced by a new type which in addition implements the interface. So the old type doesn't even exist anymore. The compiler replaces the type completely. So |
Beta Was this translation helpful? Give feedback.
-
How do you propose that work for classes declared in other assemblies? |
Beta Was this translation helpful? Give feedback.
-
The proposed code for shapes is there to show how things would actually work. i.e. it demontrates that the idea can actually be built on top of what the runtime can provide.
Right, but the problem is that interfaces today cannot do what you want. A language proposal can't just say "i want this feature to do x" but not explain how it would do that. |
Beta Was this translation helpful? Give feedback.
-
Did you read my updated version above? It basically comes down to compiler checks and already existing mechanics like the var keyword and generics. |
Beta Was this translation helpful? Give feedback.
-
Yes. it's effectively what i was mentioning before. That this is what 'shapes' already sets out to do. i.e. through generics and witness objects, you can write code that operates over the 'shape' of something (which you call the 'implicit interface'). |
Beta Was this translation helpful? Give feedback.
-
Ok but shapes seem to use some tricks with additional interfaces and structs. I thought more about some kind of preprocessing step: This preprocessing looks for all usages of implicit interfaces (or shapes) for type initialization like Then the preprocessing looks for all usages of implicit interfaces (or shapes) for generic type constraints like Then compiling can start as usual. Now there are no implicit interfaces anymore cause they were only there to check for contracts. The compiler now has not do deal with any unknown things. All thats left are So I don't really know what should be problematic here. No interfaces or structs are even needed with my proposal. Even extensions on the types would work to fulfill the contracts as long as the preprocessor knows about them. The only thing that won't work with my approach are static members for shapes. But at least my approach won't need the addition of anything beyond some preprocessing logic and the adjustment of the language syntax to allow |
Beta Was this translation helpful? Give feedback.
-
This is not possible. The constraints are defined and enforced at the CLR level. All invocations of members on that constraint are managed at the CLR level. If the compiler attempts to pass a type that doesn't officially implement the interface it will result in a runtime exception. This is why the witness struct is absolutely a requirement. It's the method that allows for the compiler to pass a non-implementing type as an implementing type in a manner that is compatible with the runtime. You can't handwave that away. |
Beta Was this translation helpful? Give feedback.
-
You can't just toss keywords at this problem. The underlying IL has no mechanism to represent this. Even if you went the full type-erasure route, you'd have no mechanism through which to actually invoke any of those members because the target type is a part of the method signature in IL, and in your case a generic method would have no idea what the target type is. |
Beta Was this translation helpful? Give feedback.
-
This can't really work though. Consider just a simple case of the method defined in il and the user calling it from their source. There is no il-code to preprocess to make it capable of handling your type. |
Beta Was this translation helpful? Give feedback.
-
I would highly recommend you try to make a pr to test your hypothesis out :-) I think you'll find quickly that even attempting that preprocessor (for source only mind you!) Will be extremely challenging :-) |
Beta Was this translation helpful? Give feedback.
-
I would highly recommend you try to make a pr to test your hypothesis out :-) I think you'll find quickly that even attempting that preprocessor (for source only mind you!) Will be extremely challenging :-) |
Beta Was this translation helpful? Give feedback.
-
Hm I thought C# generics would be resolved at compile time like C++ templates. But I guess it's this way to use generics in other assemblies as well which makes sense. Then the only possibility would be to create a specific type out of the generic type.
True the approach would only work within your own assembly. The generated IL/assembly would only contain specific types without implicit interface constraints and moreover not even the generic class/method at all. But the usecase is not about importing generic classes or methods with implicit interfaces but to use compatible types within the current assembly. But yes I see now that it might be better to implement it in another way. IL, CLR etc are not really designed to implement implicit interfaces without tricks or huge limitations. |
Beta Was this translation helpful? Give feedback.
-
I disagree. The linked proposals show ways that it could be implemented very simply, with great performance and very little limitations (if any). They benefit greatly from using the things the CLR is really good at and composing them in flexible ways to accomplish this without needing new features and without limiting to just being in your own source. |
Beta Was this translation helpful? Give feedback.
-
IIRC the one thing that the language team was interested in engaging the CLR team about was to make it possible to better specialize/inline the generic code based on the witness without having to have a separate generic type parameter for the type of the witness. I don't know if that involved improvements to IL, like allowing The rest of the pattern is the same, though. The generated witness/proxy type actually implements the interface and delegates the calls through to the target. What specialization affords is eliminating all of the overhead in doing so. |
Beta Was this translation helpful? Give feedback.
-
Yeah, from a generation perspective thse should be the same. Note, one of the last time's i checke,d the runtime was extremely good about this pattern already. i.e. i thought i saw htat it was entirely erased and was effectively the same as you handwriting it. |
Beta Was this translation helpful? Give feedback.
-
I thought about it as a powerful preprocessor with compile time type checking would be a nice thing also for other purposes. I think it can be implemented very easily with a trick. Gather all types and put them into one assembly (a type assembly) or just use all assemblies. The compiler generates all type information for you. Then you can analyze types via reflection. The preprocessor code would be very trivial. But you have to compile twice. First you only compile the type-only assembly (maybe with empty method bodies) and then use the type information from it for the preprocessing step. After that you compile again. Seems a bit strange but would allow a very easily implemented preprocessor without having to invent the wheel anew. |
Beta Was this translation helpful? Give feedback.
-
Terrific! I'm glad it's easily done. I look forward to a prototype from you that we can try out. It will really help understand the pros and cons of this proposal. Thanks! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Updated 11th Mar 2020
An interface should describe (as the name suggests) which interface an object should provide or which interface a class should implement. But the current C# language uses interfaces only to enforce interface implementation on types and checking for interface implementation is only limited to "does the class implement the interface explicitly by stating so?".
But in general I don't care if a class implements the interface by stating it through
class Foo : IFoo
but rather want to know if a class does actually implement the interface. Or in other words: does the class fulfill a structural contract.So for example take a simple interface like for points:
Now I have the Point struct from
System.Drawing
which does not implement that interface explicitly by stating so. It doesn't even know about the interface. But it does implement the interface.So if I have the following:
All of the three methods should print the position when I call it with a
System.Drawing.Point
. Cause it implementsIPoint
and the compiler is able to know that.So type matching and type constraints for interfaces should also match if the class actually implements the interface (implicit implementation) even if the class does not state so (explicit implementation).
But as changing interfaces would need a bunch of changes in the CLR and language (as @HaloFour pointed out) I propose another approach called implicit interfaces which only needs compiler changes and a small language addition.
Implementation
The implicit keyword is used in front of the interface name to denote that the type does not explicitly implement the interface but implicitly implement it. This means that the type's structure includes the structure of the interface.
There are some limitations to implicit interfaces:
var
keyword).void foo(implicit IBar bar) { }
.bar switch { implicit IBar bar => ... }
.Implicit interfaces can be used in the following cases:
implicit IBar bar = new Bar();
class Foo<T> where T : implicit IBar
An implicit interface is no real type and is not reflected in the CLR.
It is based on the mechanics of generics or the var keyword.
Explanations for point 1-2 above:
First of all the compiler checks if a given type fulfills the contracts of the interface. If not a compiler error is generated if an object or type is used which doesn't fulfill it. Only if the contract is fulfilled the following is even possible.
implicit IBar
with the real type. It is equivalent to using thevar
keyword but it denotes that the interface contract has to be fulfilled (the compiler checks that in contrast tovar
).Examples
Use cases
Structural interface type system:
Enforce a structural interface contract:
Beta Was this translation helpful? Give feedback.
All reactions