-
Notifications
You must be signed in to change notification settings - Fork 205
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
Module restrictions and mixins #1696
Comments
A quick write-up of how I'd define it without the mixin-capability. Things you can do with classes that you cannot do with other types.
I won't include "Mix in" in the list, because it's something you do with mixins. However, you can implement mixins too, so there is some overlap. Also, you can currently treat some class declarations as mixin declarations as well. That should end as soon as possible, and at the latest as part of the change introducing these capabilities. So, let's introduce modifiers:
Grammar becomes: <classHeader> ::=
`open'? (`abstract' | `interface')? `class'
| `open'? `interface'
<classDeclaration> ::=
<classHeader> <identifier> <typeParameters>?
<superclass>? <interfaces>? `{' (<metadata> <classMemberDeclaration>)* `}'
| <classHeader> <mixinApplicationClass>
<mixinDeclaration> ::= `interface'? `mixin' <identifier> <typeParameters>?
(`on' <typeNotVoidList>)? <interfaces>? `{' (<metadata> <classMemberDeclaration>)* `}' That is, we also support Declaration combinations
To make sure things match up with the declarations, we can add the following restrictions. They are not essential, they just trigger if the declared and actual capabilities of a declaration are not in sync. They could also just be warnings.
And then we introduce the restrictions that only apply outside the same compilation unit:
Finally, we want to ensure that an unimplementable interface remains unimplementable.
(The immediate super-interfaces of a mixin are those declared in an Examples// No extends, implements or instantiation.
// Only author can create the objects. Used for `enum`-like declarations.
abstract class Mine {
final int value;
Mine(this.value); // Doesn't even need to be private, but could.
}
// No extends or implements.
// Anyone can create instances, but the class is effectively final/sealed.
class Finality {
final int value;
Finality(this.value)
}
// No extends or instantiation.
// Only author can create instances, all others must create their own class
// implementing the interface.
// Likely to not have implementation.
interface SomeInterface {
final int value;
SomeInterface(this.value);
}
// No extends.
// Can instantiate or create other implementations.
interface class DefaultImplementation {
final int value;
DefaultImplementation(this.value);
}
// No implements or instantiation.
// Can only be used as a superclass, the base class of other classes.
open abstract class SuperClass {
final int value;
SuperClass(this.value);
}
// No implements.
// Ensures all instances extend this class.
open class SealedHierarchy {
final int value;
SealedHierarchy(this.value);
}
// No instantiate.
// Interface with default skeleton implementation.
open interface DefaultSkeletonImplementation {
final int value;
DefaultSkeletonImplementation(this.value);
}
// Anything goes, the current class.
open interface class Permissive {
final int value;
Permissive(this.value);
} (We will probably quickly see an |
There is a lot to unpack here. :) Since GitHub doesn't have threaded comments and issues can get unwieldy, I'll try to summarize but let me know if I miss anything important. Remove support for combining mixins with other capabilitiesMy initial pitch supports all 16 combinations, including allowing "mix-in" combined with anything else. You'd remove those and only allow mixins to be used as mixins. I'm guessing you would like to allow mixins to expose interfaces? If so, I think you're proposing:
I like the idea of factory constructors on mixins but I think it would be weird to allow that while prohibiting mixins with generative constructors or that can be subclasses. Is the mixin class-like or not? If we really want mixins to be purely mixins, then I'd keep the prohibition against factory constructors too. (Though since mixins can expose an interface, it is a little strange to have an interface type where you can't define a factory constructor at the logical place for it.) I'm not strongly opposed to separating out mixins and not, uh, mixing them with the other capabilities. But, for what it's worth, we do see users combining these capabilities in real code. And I don't think it adds much complexity to the language to support all the combinations. In the past, whenever we by-fiat exclude combinations of features that could otherwise be combined orthogonally, we tend to regret it (looking at you, named and optional parameters). So I'm inclined to just support all the combinations unless it does significantly add to the implementation complexity or cognitive load. Extend + Implement
The proposal I have supports that but spells it Legacy classes
Good call. Done: e9b2fc0 |
I'm not particularly sold on factory constructors on mixins. We allow factory constructors on classes that you can derive a mixin from, but I want to drop those entirely. There is no strong need for factory constructors on The issue with combining "mixin" with the other capabilities is that it's not orthogonal. A Any class with a superclass other than object, or any mixin with an It's worth noticing that the supertype of a class is not part of the class's public facing API, it's just one of its many interfaces. Existing code is not a reason to allow mixin+class declarations. If we assume that we allow class+mixin declarations, then we likely can't allow you to write an open interface mixin class A extends S implements I { ... } Here A cannot declare a generative constructor, because mixins can't (at least for now, much more complicated feature to allow that). It can have a default constructor, so if the declaration is It can be The I fear it's going to be very confusing to have Since (non- Not sure I can keep track. How does a |
Thinking about it more, I actually do want factory constructors on mixins. I have a larger agenda here which is that I think users should lean on mixins more often than they do instead of interfaces. A mixin is basically an interface that can have default implementations of methods, and that latter property is really useful because it makes it much easier to evolve the mixin. Maybe I'm missing something obvious (aside from unfamiliarity), but I can think of few cases where an interface in some API wouldn't be better off being defined as a mixin. The latter makes it easier to grow the type in non-breaking ways but doesn't otherwise make it harder for consumers of the API to use.
Ah, I'm definitely not strongly attached to being able to also use mixins as superclasses or (non-factory) constructible types. It just felt weird and somewhat arbitrary to me to exclude those combinations. Especially when I do think it's useful for mixins to support factory constructors, static members, and interfaces. At some point, it just seems more natural to let you mix and match any damn thing you want.
+100.
Yeah, I'm fairly persuaded that some of these combinations and the complex restrictions on them could be confusing.
From the user's perspective, there is no way to tell whether In practice, I don't think users have the same strong conceptual distinction between "class" and "mixin" that the spec does. The spec says a mixin is basically a function that produces classes given a superclass, which is a nice formalism. But I think users are basically like "A mixin is a kind of class thing that I'm allowed to put after
This discussion makes me realize that combining mixins with classes could be very confusing in another way too: class A {
a() {}
}
mixin class B extends A {
b() {}
}
class C with B {} I strongly suspect that users would be very surprised to discover that C gets
OK, thinking about it more, it seems like there are three fundamental entities in Dart:
Any of these entities may also have other static members (including factory constructors), but those are essentially unrelated to the entity itself. They just use its name as a namespace. (There are also "classes" with no generative constructors but those are basically not entities as much as just pure namespaces.) There are four combinations of those entities:
So what you suggest is we only allow the combinations that don't get confusing: interface+mixin and interface+class. So the allowed combinations would be:
Another way to say this is that it is fundamental that mixins do not have superclasses and it is fundamental that anything you can construct or extend does have a superclass. Thus those capabilities are strictly incompatible. I think I'm OK with that, though I'd like to hear what others on the language team think. |
Not having composite mixins is another issue, and it's true that users would probably not recognize that classes with superclasses aren't composite mixins. I'd already assumed we wouldn't allow mixins with superclasses, and only allow
(So only allow both of Don't think it would be worth it to have two mostly separate syntactic categories and then having them intersect at a single point anyway.. So, wohoo! 😁
Yep. That's how I see it too. Interfaces carry signatures, including super-interfaces (with potentially other signatures) that you can up-cast to. They define the operations that you can statically be allowed to perform on a type. Classes carry implementation, which means the superclass chain back to Mixins are abstractions over the units of implementation in the superclass chain of classes. Since each step in the chain has some implementation methods, some new super-interfaces and their own interface, so does a mixin. It abstracts over the superclass using a synthetic interface (the combined interface of the
Yes, that does look like it matches my table from above. |
tl;dr Drop @munificent wrote:
I like it! First I'll mention one exception, and then give some reasons why I like the rest: We should not distinguish between We could interpret the situation differently, and say that (outside the declaring module) a
I think that should work. In particular, we do allow factory constructors in a class which is used to derive a mixin. A mixin application will introduce a bunch of forwarding generative constructors, but they are not affected by the rule that a constructor (also As mentioned, I like the rest of it! In particular: I very much support a decision to keep mixins and classes clearly separated. I like that we don't have "built-in" Instance creation for a mixin: It could actually work in some cases (for |
I have no problem with It is not a problem that |
+1 to all this. Yes, at the type checking level, every class/mixin still defines an interface and you will get that as a superinterface if you extend a class or apply a mixin. But the part that I care about is that external users cannot get the interface for a class or mixin without actually inheriting the concrete methods if the author doesn't want you to. In other words, I'm totally OK with saying that the difference between I think it's useful to support this distinction because:
I'm glad you like the other modifiers! I used your proposal as a reference when putting this together. :) |
OK, I've updated the proposal: 02790af Closing this because I think there is no more known action to take but feel free to reopen if you'd like to discuss more. |
Just one thing to consider: Given that you, @munificent and @lrhn, are both happy about the distinction between This helps keeping the concepts separate: You get an implementation (and it can't be in your |
Requiring all mixin members to implement an inherited interface signature seems wasteful. Or, rather, I don't see what it's trying to achieve. If you want to introduce a method in a mixin, you'd have to first declare another interface with that member, then make the mixin implement it. I don't see what that saves. |
The point is that
then it should be an |
The point in introducing new methods in non- It's not a point to avoid changing your interface, that's perfectly fine. If you extend a non- |
But that's a silly property, because it doesn't mean that it will ever be executed, it doesn't tell us anything. |
I think the point in having a non-interface class is that its subtype hierarchy will be a tree, not a DAG, and this means that we can rule out the existence of instances that are subtypes of multiple unrelated nodes in that tree. This could, for instance, enable faster dispatch. |
Is that true? The class or its superclasses may implement multiple interfaces. The fact that the class itself isn't an interface just means that all subclasses of the class will form a tree rooted at the class. |
That's exactly the property I had in mind. One community where the difference between tree-shaped subtype graphs and other subtype graphs has been explored extensively is the Java/JVM community. In Java, an interface invocation ( In Dart (when compiling to machine code), I suspect that we already have fixed offsets, because they are chosen in a step which is common to the entire program (I think we're using something like selector coloring and row displacement as described in https://webhome.cs.uvic.ca/~nigelh/Publications/cdt95.pdf). However, we might be able to improve on the startup time if we could use the traditional vtable techniques, at least for some methods, and it is also possible that the techniques currently used in the VM aren't quite as fast or space-preserving as the traditional vtable lookups. In any case, I think it's worth being aware of the potential performance benefits that we could have, based on tree-shaped subtype graphs. |
Comments on https://github.com/dart-lang/language/blob/master/working/modules/feature-specification.md
The grammar declarations contain:
I do not want to conflate mixins and classes. Those are two separate kinds of declarations, and I would very much like to get rid of the current ability to mix in classes (q.v. #1643). If the
mixin class
is intended to support the current mixin-able classes, I'd just remove it and not allow that. If we touch this area, that's a very good time to remove the ability to mix in classes entirely. (Any time is a good time IMO, the best time would be yesterday!)I'm also not sure it makes sense to extend a mixin declaration.
Not yet, at least, we don't have composite mixins, and you can't do
extends mixin
anyway. I'd remove the'open'?
frommixinDeclaration
for now, writingopen
in front doesn't mean anything unless we add the ability to actually extend them.(Unless lacking
open
would mean that you cannot extend something mixing in the mixin. I'm not sure that's a good idea, but it would be consistent. We'd then need a different word if we ever introduce composite mixin declarations, so I think I'd prefer to reserveopen
for that).That means that anywhere later where it talks about instantiating mixins, or mixins having generative constructors (any constructors, really), it should be dropped. Mixins do not have constructors, and cannot be instantiated.
(I also think
abstract interface class
should be a possibility, cannot instantiate, can extend or implement).Like slightly later where it states:
You can't instantiate a mixin declaration. It must be mixed onto a superclass before it even becomes a class, which is what can be instantiated. While we still, effectively, have
mixin class
declarations, it's the class you are instantiating, not the mixin which can be derived from it.(The usual model for the first sentence is to say:
So, don't look at declarations, look at implementation vs interface. We might need another name for "interface" if we have classes without an interface, they still have a signature, just not one you can
implements
).And much later:
Yes, it's currently correct. We could allow factory constructors (we do allow static methods, and factory constructors are basically static methods with trickier type parameters and a fixed return type), and with
interface mixin
declarations we might want to allow factory constructors.If we allow any use "internally", but only declared uses externally, then it does become an issue that we separate mixins and class declarations.
You cannot do
interface Foo { ... }
and use it as a mixin, because it is not a mixin and you can't mix-in non-mixins (please, do not allow that!). But if you writeinterface mixin Foo { ... }
then the type is publicly a mixin.Instead of allowing you to internally ignore any constraints, should we instead allow you to declare something as
interface _mixin
where it's only privately a mixin?About:
That sounds like an implicit language versioning. Why not just use explicit language versioning, and migrate existing code by adding
open interface
in front of all classes, and maybe combine with #1643 to migrate code extending a "mixin class
" towith Name
instead ofextends Name
.(Might be worth mentioning that you can make non-extensible classes today by not exposing a public generative constructor.)
The text was updated successfully, but these errors were encountered: