Skip to content
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

Closed
lrhn opened this issue Jun 22, 2021 · 17 comments
Closed

Module restrictions and mixins #1696

lrhn opened this issue Jun 22, 2021 · 17 comments
Labels

Comments

@lrhn
Copy link
Member

lrhn commented Jun 22, 2021

Comments on https://github.com/dart-lang/language/blob/master/working/modules/feature-specification.md

The grammar declarations contain:

classDeclaration         ::= 'open'? 'interface'? 'mixin'? 'class' // ...
mixinDeclaration         ::= 'open'? 'interface'? 'mixin' // ...

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'? from mixinDeclaration for now, writing open 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 reserve open 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:

  • It is a compile-time error to invoke a generative constructor of a type if the type defines or inherits any unimplemented abstract members. You can directly construct anything internally if it wouldn't cause a problem to do so, even an interface or an abstract class. TODO: Even a mixin?

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:

  • It's a compile-time error to invoke a generative constructor of a class if the class does not have a valid implementation for all members of its interface.

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:

  • It is a compile-time error if a public-named type marked mixin defines any constructors. TODO: Is this restriction correct?

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 write interface 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:

Capabilities on legacy classes

The above syntax means that it an error to implement, mixin, or extend a class declared just using class. This would break nearly all existing Dart code. To avoid that, we specify that Dart classes that are not in modules are implicitly treated as if they were declared as open interface mixin class.

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" to with Name instead of extends Name.

(Might be worth mentioning that you can make non-extensible classes today by not exposing a public generative constructor.)

@lrhn lrhn added the modules label Jun 22, 2021
@lrhn
Copy link
Member Author

lrhn commented Jun 23, 2021

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.

  • Instantiate: invoke generative constructor.
  • Extend: use as superclass in class declaration or mixin application.
  • Implement: use as interface in implements clause.

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:

  • interface on class and mixin declarations which allows using as an interface in an implements clause (and you cannot do so otherwise).

  • open on class declarations which allows you to extend the class, using it as a superclass in a class declaration, including a mixin application (and you cannot do so otherwise).

  • abstract on a class which prohibits instantiation (which is not new, but unlike the current abstract, the restriction does not apply to object creations inside the same compilation unit).

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 class C = … syntax, now as open interface class C = … or open abstract class C = … and introduce open interface C = ….

Declaration combinations

Extend Implement Instantiate
abstract class
mixin
class
interface
interface mixin
interface class
open abstract class
open class
open interface
open interface class

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.

  • It's a compile-time error if an instantiable or extensible class declaration does not have a public generative constructor (whether declared or implicit). Without a generative constructor, you can't instantiate or extend the class anyway. This restriction can be bypassed by adding C.unusable(Never x);. 

  • It's a compile-time error if an instantiable class declaration does not have a concrete implementation of every member in its interface. This mirrors the current requirement that non-abstract classes implement their interface. We still need to make it an error to instantiate any non-instantiable class not implementing its interface inside the same compilation unit.

  • It's a compile-time error if an implementable class with a non-library-private name has (declare or inherit) any members with a library private name. Also tricky. One alternative would be to allow a different name for the public interface, interface A class B { … } would omit the private members of B inA. Would it be possible to completely remove private members from interfaces and rely on subclassing to share those?

And then we introduce the restrictions that only apply outside the same compilation unit:

  • It's a compile-time error if a class declaration C contains D as a superclass (extends D or class ... = D with ...), D is not declared as open (so D is not extensible), and C is not declared in the same compilation unit as D.
  • It's a compile-time error if a class or mixin declaration C contains an implements clause listing a super-interface D, D is not declared as an interface (so D is not implementable), and C is not declared in the same compilation unit as D.
  • It is a compile-time error if an object construction expression C(…), C.named(…), possibly prefixed by new or const, denotes a generative constructor, C is declared as abstract class, or as interface-not-followed-by-class (so C is not instantiable), and the expression does not occur in the same compilation unit as the declaration of C. (TODO: It'll also be a compile-time error to tear-off such a constructor.)

Finally, we want to ensure that an unimplementable interface remains unimplementable.

  • It is a compile-time error if a class or mixin C has the interface of a class or mixin D as an immediate or transitive super-interface, D is not implementable, C is implementable, and C is not declared in the same compilation unit as D.

(The immediate super-interfaces of a mixin are those declared in an implements clause and those declared in the on clause).

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 @interfaceForTesting annotation on an implementable class, preventing its use in implements clauses outside of tests, so it can still be mocked. The alternative would be to somehow allow the test to temporarily belong to the same compilation unit.)

@munificent
Copy link
Member

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 capabilities

My 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:

// Keep:
mixin                       // Yes Mix-in
interface mixin             // Yes Mix-in Implement

// Remove:
open mixin                  // No  Mix-in Extend
open interface mixin        // No  Mix-in Implement Extend
mixin class                 // No  Mix-in Construct
open mixin class            // No  Mix-in Extend Construct
interface mixin class       // No  Mix-in Implement Construct
open interface mixin class  // No  Mix-in Implement Extend Construct
  • It is a compile-time error if a public-named type marked mixin defines any constructors. TODO: Is this restriction correct?

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.

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

(I also think abstract interface class should be a possibility, cannot instantiate, can extend or implement).

The proposal I have supports that but spells it open interface (which I like the sound of).

Legacy classes

Capabilities on legacy classes

The above syntax means that it an error to implement, mixin, or extend a class declared just using class. This would break nearly all existing Dart code. To avoid that, we specify that Dart classes that are not in modules are implicitly treated as if they were declared as open interface mixin class.

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" to with Name instead of extends Name.

Good call. Done: e9b2fc0

@lrhn
Copy link
Member Author

lrhn commented Jun 24, 2021

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 mixin declarations (even if they can have interfaces).

The issue with combining "mixin" with the other capabilities is that it's not orthogonal.

A mixin declaration has an on type, a class has a supertype. We only allow you to use a class a both when both the on type and the supertype are trivial, meaning they are both Object, and you don't need to write them.

Any class with a superclass other than object, or any mixin with an on type other than Object, cannot be both a class an a mixin. (We probably could allow class declarations with a super-type other than Object to be used as mixins as well, by using the supertype as an on-type, because we once did that, but without having to mark the class as mixin, it was a pain when it got used as a mixin when it wasn't intended for it).

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.
The on type of a mixin is part of its public-facing API. (That's why using a non-intended class as a mixin broke abstraction and prevented the class owner from changing the supertype).

Existing code is not a reason to allow mixin+class declarations.
Every current use if a class+mixin combination has Object as supertype/on-type. There is no other possibility.
That means that you can change extends TheThing with with TheThing and keep trucking. You don't need to use it as a class, ever. Making it a mixin is a migration, but not a change. (And it's a migration I've been asking for since we introduced mixin declarations.)

If we assume that we allow class+mixin declarations, then we likely can't allow you to write an on type for the mixin-part. (That's one of my points of opposition to allowing it).
We could restrict it to only having Object as superclass too, but that's so restrictive that I don't think it's worth it at all.
So something like:

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 open, it must have a default constructor, and therefore no factory constructors. Somewhat restricted, but no worse than "mixin class"es today.

It can be interface or abstract or neither. If it's neither, it should probably have to have the default constructor.

The on type of the mixin will be S. If S is a mixin application like B with M, then that defines an anonymous class that no-one can implement. Probably not what you need.
The VM originally treated that as a requirement of the intersection type B&M instead, allowing any superclass implementing both. We could do that, but I'd still prefer for users to have to use mixin declarations with on types if they want multiple supertypes. Interpreting B with M as anything but that anonymous class makes it a breaking change to extract it to class Super = B with M; open interface mixin class A extends Super ... is then something else.
We have defined the way multiple on-types are combined in detail in the mixin declaration spec, and that may not match the interface of B with M precisely, because parallel combination is not the same as sequential layering (but probably will match up to equivalence of top-types or something similar).

I fear it's going to be very confusing to have mixin declarations with on types, and mixin class declarations without.

Since (non-abstract) class seems to denote ability to instantiate (interface class vs interface), and if we can also have interface mixin class which is a class declaration which can be instantiated, implemented and mixed in, why is interface mixin not a class declaration which can be implemented and mixed in, but not instantiated?
(Because it's a mixin declaration which can be implemented and mixed in).
But open interface mixin would be a class declaration, because it can be extended. (But can't declare generative constructors).

Not sure I can keep track. How does a mixin declaration differ from a class declaration (with or without the class keyword) that can be mixed in?
(Is it really a good idea to drop class from interface and open interface, if they can be combined with mixin. Should we have open mixin interfaces to match open mixin interface class, just without instantiation?)

@munificent
Copy link
Member

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 mixin declarations (even if they can have interfaces).

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.

The issue with combining "mixin" with the other capabilities is that it's not orthogonal.

A mixin declaration has an on type, a class has a supertype. We only allow you to use a class a both when both the on type and the supertype are trivial, meaning they are both Object, and you don't need to write them.

Ah, on clauses are a good point. If we wanted to combine mixin with construct or extend capabilities, we'd probably have to have some structural limitations around mixins with on types in order to make that safe.

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.

That means that you can change extends TheThing with with TheThing and keep trucking. You don't need to use it as a class, ever. Making it a mixin is a migration, but not a change. (And it's a migration I've been asking for since we introduced mixin declarations.)

+100.

I fear it's going to be very confusing to have mixin declarations with on types, and mixin class declarations without.

Yeah, I'm fairly persuaded that some of these combinations and the complex restrictions on them could be confusing.

Since (non-abstract) class seems to denote ability to instantiate (interface class vs interface), and if we can also have interface mixin class which is a class declaration which can be instantiated, implemented and mixed in, why is interface mixin not a class declaration which can be implemented and mixed in, but not instantiated?
(Because it's a mixin declaration which can be implemented and mixed in).

From the user's perspective, there is no way to tell whether interface mixin gives you a class or a mixin. Because it gives you a thing that, either way, you can't extend or construct, so there's no affordance that would let you distinguish them.

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 with".

But open interface mixin would be a class declaration, because it can be extended. (But can't declare generative constructors).

Not sure I can keep track. How does a mixin declaration differ from a class declaration (with or without the class keyword) that can be mixed in?

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 b() but not a(). OK, I think I'm sold on not allowing combining mixin stuff and class stuff together.

(Is it really a good idea to drop class from interface and open interface, if they can be combined with mixin. Should we have open mixin interfaces to match open mixin interface class, just without instantiation?)

OK, thinking about it more, it seems like there are three fundamental entities in Dart:

  • An interface is a set of method signatures and a set of superinterfaces.

  • A mixin is a set of methods (some of which may be abstract), and a potentially empty on clause.

  • A class is a concrete superclass chain going all the way to Object and one or more generative constructors. You may or may not be able to invoke the generative constructors directly.

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:

  1. You can define an interface from a class by taking the signatures of the methods on the class, and the interfaces of all of its superinterfaces and superclass as its superinterfaces.

  2. You can define an interface from a mixin basically the same way.

  3. Dart today lets you take the mixin of a class by taking the class declaration and discarding its superclass. But this is pretty confusing for users and we'd like to move away from it.

  4. Likewise you can derive an interface and a mixin from a class by doing both 3 and 2. But, again, it's pretty confusing.

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:

class                               // 63.93% Construct
abstract class                      // 14.09% (none)
interface                           //  9.77% Implement
open abstract class                 //  6.47% Extend
interface class                     //  2.36% Implement Construct
mixin                               //  1.25% Mix-in
open class                          //  0.86% Extend Construct
open interface                      //  0.76% Implement Extend
open interface class                //  0.20% Implement Extend Construct
interface mixin                     //  0.09% Mix-in Implement

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.

@leafpetersen @eernstg @jakemac53 @natebosch ?

@lrhn
Copy link
Member Author

lrhn commented Jun 25, 2021

I strongly suspect that users would be very surprised to discover that C gets b() but not a(). OK, I think I'm sold on not allowing combining mixin stuff and class stuff together.

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 mixin class if the superclass is Object.
That would mean removing the explicit mixin declaration and combining it into an interface mixin class declaration
which can have both extends and on, and then reining it in using rules like;

  • It's a compile-time error if a declaration has an on clause and is not marked mixin, or is instantiable or extensible.
  • It's a compile-time error if a declaration has an extends or with clause and is marked mixin.

(So only allow both of mixin class if there is no extends or on clause).

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! 😁

OK, thinking about it more, it seems like there are three fundamental entities in Dart:

An interface is a set of method signatures and a set of superinterfaces.

A mixin is a set of methods (some of which may be abstract), and a potentially empty on clause.

A class is a concrete superclass chain going all the way to Object and one or more generative constructors. You may or may not be able to invoke the generative constructors directly.

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 Object, and has generative constructors which allow further extension (they can have zero generative constructors, then it's effectively just an interface).
Each class also defines an interface. (Abstract declarations goes into the interface, not the implementation).
A non-abstract class must implement its own interface.

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 on clause interfaces), which the actual superclass must satisfy the signatures and superinterfaces of.

So the allowed combinations would be:

Yes, that does look like it matches my table from above.
SGTM!

@eernstg
Copy link
Member

eernstg commented Jul 1, 2021

tl;dr Drop interface mixin, otherwise it's great! rd;lt

@munificent wrote:

class                               // 63.93% Construct
abstract class                      // 14.09% (none)
interface                           //  9.77% Implement
open abstract class                 //  6.47% Extend
interface class                     //  2.36% Implement Construct
mixin                               //  1.25% Mix-in
open class                          //  0.86% Extend Construct
open interface                      //  0.76% Implement Extend
open interface class                //  0.20% Implement Extend Construct
interface mixin                     //  0.09% Mix-in Implement

...
I think I'm OK with that, though I'd like to hear what others on the language team think.

I like it! First I'll mention one exception, and then give some reasons why I like the rest:

We should not distinguish between mixin and interface mixin: The problem is that a mixin application is defined to use the mixin as a superinterface, and this means that a mixin would be a mixin that can't be mixed in! (Of course, that's the only thing it could do, so it's useless.) If we wish to protect the world completely from using a mixin then we should simply give the declaration a private name, or place it in a private module. So I'd prefer that we drop the support for the syntax interface mixin, and give that semantics to the plain mixin declaration.

We could interpret the situation differently, and say that (outside the declaring module) a mixin can occur in a mixin application, it just can't occur in an implements clause (and then we'll find some weasel-words to say that the desugared meaning of the mixin application can still have the mixin in its implements clause). However, developers can get the same effect by applying the mixin and then overriding each of its methods to suit their purpose. We can play tricks with auxiliary methods in order to enable something that works like super-invocations, thus bypassing the method implementations in the mixin. So we can't force the method implementations of the mixin upon out-of-module clients, and hence there's little value in having the distinction between mixin and interface mixin.

Thinking about it more, I actually do want factory constructors on mixins.

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 factory) prevents the default constructor from being created.

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 new M() we'd implicitly create class C = Object with M; and make it new C(), if that's not an error), but it is not obvious how we should deal with on clauses with multiple classes. Better not go there at all. A factory constructor would do the job just fine, and developers can do whatever they want in the cases where it isn't obvious which class to use.

@lrhn
Copy link
Member Author

lrhn commented Jul 1, 2021

I have no problem with interface mixin M vs mixin M.
The latter ensures that anything implementing M also inherits the implementation from mixin M, because that's the only way you can create a class implementing M.
The former allows you to implements M anywhere.

It is not a problem that X with M implements M (aka. "has M as superinterface") as long as you can't write implements M unless the mixin is declared as interface. That's how it works for classes too.
The desugaring of mixin application into something containing implements M is an implementation/specification artifact. The mixin application contains the method implementations of M and has M as superinterface, and that's OK. You're still enforcing that the implementation and interface are connected, where an interface declaration allows you to use the interface without the implementation.

@munificent
Copy link
Member

It is not a problem that X with M implements M (aka. "has M as superinterface") as long as you can't write implements M unless the mixin is declared as interface. That's how it works for classes too.

The desugaring of mixin application into something containing implements M is an implementation/specification artifact. The mixin application contains the method implementations of M and has M as superinterface, and that's OK. You're still enforcing that the implementation and interface are connected, where an interface declaration allows you to use the interface without the implementation.

+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 mixin and interface mixin is literally just that the former makes it a compile error to have the mixin appear in an implements clause outside of the module where the mixin is defined.

I think it's useful to support this distinction because:

  • It means authors of the mixin can add new methods to the mixin without it being a breaking change. (Well, much likely to be. It could still be a breaking change if the class applying the mixin also has a method with the same name but a different signature. But there's only so far we can go.)

  • It means that within the module, if I have an instance of M, I know it's a concrete instance of my M. That in turn means I can reliably access private members declared on M which an external implementation of M's interface would lack.

I'm glad you like the other modifiers! I used your proposal as a reference when putting this together. :)

@munificent
Copy link
Member

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.

@eernstg
Copy link
Member

eernstg commented Jul 2, 2021

Just one thing to consider: Given that you, @munificent and @lrhn, are both happy about the distinction between mixin and interface mixin, would it be helpful to make mixin an "implementation only" concept by insisting that every member declared in a mixin must be an override of some member in one of its superinterfaces?

This helps keeping the concepts separate: You get an implementation (and it can't be in your implements clause, and you don't need that anyway because there are some other interfaces yielding the same members, unless private), or you get an interface which is bundled with an implementation, and you can use it (using with) or avoid using it (using implements).

@lrhn
Copy link
Member Author

lrhn commented Jul 2, 2021

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.
And if you want a mixin without an interface (mixin rather than interface mixin), one with implementation that you can only get by mixing it in and then inheriting it, it seems spurious to have to introduce an interface to do that.

@eernstg
Copy link
Member

eernstg commented Jul 2, 2021

The point is that

If you want to introduce a method in a mixin,

then it should be an interface mixin, because it (the mixin, and nothing else) will change your interface by adding that new method to it. So this leaves the plain mixin as a pure implementation provider, and it won't pollute your interface with anything new (if it adds something then you can get that something from the superinterfaces of the mixin).

@lrhn
Copy link
Member Author

lrhn commented Jul 2, 2021

The point in introducing new methods in non-interface class or mixin declarations is that you are guaranteed that anybody having that method (implementing your interface) also has your implementation in their superclass chain, because they can't get that member into their interface by implements.

It's not a point to avoid changing your interface, that's perfectly fine. If you extend a non-interface class, you get all its members too. The point is to ensure that anyone else which also has that interface also has the implementation.

@eernstg
Copy link
Member

eernstg commented Jul 2, 2021

anybody having that method (implementing your interface) also has
your implementation in their superclass chain

But that's a silly property, because it doesn't mean that it will ever be executed, it doesn't tell us anything.

@eernstg
Copy link
Member

eernstg commented Jul 2, 2021

If you extend a non-interface class, you get all its members too

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.

@munificent
Copy link
Member

I think the point in having a non-interface class is that its subtype hierarchy will be a tree, not a DAG

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.

@eernstg
Copy link
Member

eernstg commented Jul 26, 2021

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 (invokeinterface) is more expensive than a class invocation (invokevirtual), because the class invocation can rely on a vtable which is specific to the run-time type of the receiver and yet has exactly the same offsets as those of the vtable of the statically known type (which is a class). So we just look up the code address in the given vtable at a statically known offset and call that code. With invokeinterface there could be many different classes implementing the given interface, and they cannot use the same offset for the same method (which is an implementation of a method declared by the interface), because they were compiled separately, and they may be loaded at different times.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants