Skip to content

[Class Modifiers] Adding mixin confuses positive and negative capabilities again #2723

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

Closed
leafpetersen opened this issue Dec 15, 2022 · 17 comments
Labels
class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. patterns Issues related to pattern matching.

Comments

@leafpetersen
Copy link
Member

The most recent proposal for capability modifiers adds a mixin keyword which allows using a class as a mixin. Prior to that, we were able to finesse the issue of whether interface and base were positive or negative capabilities, since you never had to specify more than one of them. This was convenient, because they are positive sounding keywords, and it is very confusing to have to reason "backwards" from negative capabilities (see discussion in #2595). The proposal in that state had the property that if any capabilities were listed, then all of the available capabilities were listed.

Adding mixin as currently specified breaks this model, because the specification makes interface and base negative capabilities, and makes mixin a positive capability. That means that combining interface or base with mixin requires reasoning about "residual" capabilities. That is, one says that base class is class which can be extended, and interface class is one which can be used as an interface. But a mixin class is a class which can be extended, implemented, or mixed in. So it no longer lists all of its capabilities. When you add a modifier, you therefore have to start reasoning negatively: adding interface takes away the ability to extend. So interface mixin class is a class which cannot be extended (but can be used as a mixin or an interface). This is confusing to start with (switching from positive to negative) but what seems worse is that if I start thinking of these as negative capabilities, then I naturally would expect that since I can have a base mixin class and an interface mixin class, then I should be able to have a base interface mixin class which can only be mixed in or constructed. This follows naturally: I have a mixin class, I can remove the ability implement by adding interface and I can remove the ability to extend by adding base, so surely I can add both? The specification currently does not allow this combination syntactically.

I see three possible ways out of this.

First, we could choose to make interface, mixin and base behave uniformly as monotonic positive capabilities. That is, a mixin class can only be mixed in, and you have to say interface mixin class if you want to be able to use it as an interface. You then need to be able to say interface base mixin class if you want to allow everything, because plain class doesn't allow mixing in anymore.

Second, we could just choose not to allow mixin to be combined with anything else. Note that:

  • interface mixin class (and similarly interface mixin) is not very useful. Stopping someone from extending something while allowing them to still mix it in (which they can do trivially on Object) doesn't seem to add anything.
  • base mixin class (and similarly base mixin) could be useful. It's nice to be able to stop someone from using something as an interface.
  • sealed mixin class (and similarly sealed mixin) don't seem to make sense to me, since that breaks exhaustiveness.
    So only one of the combinations is really useful.

Third, we could say that a mixin class can be extended and mixed, but not implemented, unless it is marked with `interface.

  • base mixin is not allowed (there's no point)
  • sealed mixin is not allowed (likewise)
  • interface mixin is allowed, and becomes the "non-breaking" migration path: if you want to preserve the behavior from before this language change you need to change class to interface mixin class.

The third option is appealing to me.

  • It gets rid of the combinations that seem to me to be pointless.
  • It makes mixin behave in the most useful way: you must mixin (or extend if you want) and can't implement it.
  • You can still get all of the capabilities if you want: interface mixin class.

The one wrinkle with option 3 that I see is that it would naturally suggest that non-class mixins should also not be allowed to be implemented unless marked with interface. This would be another breaking change.

cc @munificent @eernstg @lrhn @jakemac53 @natebosch @kallentu @stereotype441 @mit-mit

@leafpetersen leafpetersen added patterns Issues related to pattern matching. class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. labels Dec 15, 2022
@lrhn
Copy link
Member

lrhn commented Dec 15, 2022

I think it's OK as currently defined.

The mixin is not a modifier on the declaration, it is the declaration. You are declaring a mixin and a class at the same time.

We have a class declaration for declaring classes, a mixin declaration which declares a mixin, and a mixin class declaration which declares both at the same time. It's not a class with a positive mixin modifier, not any more than it's a mixin with a positive class modifier. It must satisfy the requirements of both. It's really two declarations in one.

I'd say that you cannot mix-in a non-mixin class, not even in the same library. It's not just an added restriction on the use of a class to not have mixin, a non-mixin class is-not-a mixin, not any more than mixin Foo on Bar {} is a class that you cannot extend.

I think it works, both because of Dart's history and the stand-alone mixin declaration, and because of the words used.
Both class and mixin are nouns (the way we use mixin in Dart), and final, sealed and base are modifiers. The odd one out, if anyone, is really interface.
(And I have proposed allowing interface Foo as shorthand for abstract interface class Foo.)

@leafpetersen
Copy link
Member Author

I'd say that you cannot mix-in a non-mixin class, not even in the same library.

This isn't how it is currently specified. As currently specified, sealed mixin class is a no-op, and should be eliminated (it adds nothing). sealed mixin seems of very limited use, but maybe.

And interface mixin class and interface mixin are flat out useless, there's no reason to allow them that I can see.

@lrhn
Copy link
Member

lrhn commented Dec 15, 2022

An interface mixin class can still be extended and mixed in inside the same library. An interface mixin can be mixed in inside the same library.

If we allow non-mixin classes to be mixed in inside the same library, then I agree that the mixin does nothing.
(If we assume that interface mixin prevents mixing in, which is reasonable.)

I'd prefer to not allow using a non-mixin class as a mixin, even in the same library.
And I'd prefer to change the specification if it allows it today.
I'd say that you can't mix in a class, only a mixin, and if you don't declare Foo as a mixin, it's not one.
The same as you can't extend a mixin, only a class.
If you declare it as mixin Foo or mixin class Foo, it is a mixin (and also a class in the latter case).

The disadvantage of that would be that you can't declare an open class that only you can mix in. If you make it mixin-able at all, it's mixin-able to everybody who can extend the class.

The (big, IMO) advantage is that we no longer have to do seemingly random checks on a class to see if it can be used as a mixin, whether it wants to or not. Instead, a mixin declaration simply cannot declare a generative constructor. It cannot have an extends clause. A class also cannot have an on clause, so a mixin class cannot have any of these. That's not something we check where you try to mix in the class, we stop you the moment you put a generative constructor in the mixin declaration (or a mixin on the class with the constructor).
Much safe. Much more explicit.

The reason we got down to three modifiers is that we only had to cover four cases (can extend × can implement), and we can use "no modifier" for one of them.

If we consider mixin a modifier, then we have eight cases, and need seven modifiers, if we still only want to use one modifier.

Ignoring that "mixinability" is a property of a class, and one that we want to control, won't remove the problem, it'll just force people to deliberately add code that prevents mixing in (generative constructor). And if you forget, it becomes breaking to add one later.

I think my view here, that mixin is a declaration, not a modifier, and that you can only mix in mixins, is at least adequate in providing and controlling the functionality we already have today.
It prevents accidentally allowing a mix-in, and later accidentally breaking code that does so.
And the explanation of "it's both a class and a mixin, and you can only mix in mixins" is simple enough that people can understand it if necessary.

I expect the mixin class combination to be extremely rare. It's there if you want it, but it's not going to be heavily used.

I still do have some problems with the modifiers' meaning wrt. mixins.
Our combinations of capabilities (outside the library) and modifiers on a plain class are:

  • class - extend, implement, instantiate
  • abstract class - extend, implement
  • base class - extend, instantiate
  • abstract base class - extend
  • interface class - implement, instantiate
  • abstract interface class - implement
  • final class - instantiate
  • abstract final class -
  • sealed class - instantiate, exhaust
  • abstract sealed class - exhaust

The problems come when I try to apply base, interface and final to a plain mixin declaration, because it seems (to me, now) like they should apply to the default capabilities of the mixin (implement, mix-in/inherit implementation), not just what they mean on a class (implement, extend/inherit implementation). So, treating mixing-in the same as extends for classes.
(There is no abstract since mixins cannot be directly instantiated anyway.)

  • mixin - implement, mixin
  • base mixin - mixin
  • interface mixin - implement
  • final mixin -
  • sealed mixin - exhaust

They should probably do the same for a mixin class too, then:

  • mixin class - extend, implement, instantiate, mixin
  • abstract mixin class - extend, implement, mixin
  • base mixin class - extend, instantiate, mixin
  • abstract base mixin class - extend, mixin
  • interface mixin class - implement, instantiate
  • abstract interface mixin class - implement
  • final mixin class - instantiate
  • abstract final mixin class -
  • sealed mixin class - instantiate, exhaust
  • abstract sealed mixin class - exhaust

So a bare class/mixin/mixin class and base allows implementation inheritance, interface and final prevents it, and both extending and mixing-in counts as implementation inheritance. (I actually think that makes good sense).

Inside the same library, you can do everything to a class that you can to a plain class, and you can do everything to a mixin that you can to a plain mixin. And you can do everything to a mixin class that you can to either a mixin or a class.
But you can't mix in a non-mixin class.

@leafpetersen
Copy link
Member Author

@lrhn I don't really understand your class tables. You have a large number of entries for which you are allowed to add mixin, but for which it does nothing (e.g. interface class -> interface mixin class). This seems wrong.

For the mixin declaration tables:

  • base mixin - mixin
    • I don't like this because I have to think about this as a negative capability. That is, base here doesn't mean "has the extends capability", it means "take away the implements capability" which is precisely what I find confusing.
  • interface mixin - implement
    • Why would I ever use this instead of interface class?
  • final mixin -
    • Why would I ever use this instead of final class?

@lrhn
Copy link
Member

lrhn commented Dec 15, 2022

Adding mixin does something: It allows you to mix in the declaration inside the library. If you don't write mixin, you cannot, not even inside the same library, because it is not a mixin. (Again, I want to disallow mixing in anything which is not declared as a mixin. Even inside the same library. Plain classes do not have imply mixins, ever.)

Outside the library, interface makes you not be able to inherit implementation, which means not extending or mixing in.

So, total table that I want, probably not what we currently have:

Declaration Inside library Outside library
class extend, implement, instantiate extend, implement, instantiate
base class extend, implement, instantiate extend, instantiate
interface class extend, implement, instantiate implement, instantiate
final class extend, implement, instantiate instantiate
abstract class extend, implement extend, implement
abstract base class extend, implement extend
abstract interface class extend, implement implement
abstract final class extend, implement
(abstract) sealed class extend, implement, exhaust exhaust
mixin implement, mix-in implement, mix-in
base mixin implement, mix-in mix-in
interface mixin implement, mix-in implement
final mixin implement, mix-in
sealed mixin implement, mix-in, exhaust exhaust
mixin class extend, implement, instantiate, mix-in extend, implement, instantiate, mix-in
base mixin class extend, implement, instantiate, mix-in extend, instantiate, mix-in
interface mixin class extend, implement, instantiate, mix-in implement, instantiate
final mixin class extend, implement, instantiate. mix-in instantiate
abstract mixin class extend, implement, mix-in extend, implement, mix-in
abstract base mixin class extend, implement, mix-in extend, mix-in
abstract interface mixin class extend, implement, mix-in implement
abstract final mixin class extend, implement, mix-in
(abstract) sealed mixin class extend, implement, mix-in, exhaust exhaust

As for base taking away the implements capability, that's what base class does too.
A base mixin, is a normal mixin inside the libray, which is restricted outside the library to only inheriting implementation, not being implemented differently. Just like a base class, it's exactly the same thing.

The apparent difference might come from you not usually considering mixin declarations as a source of an interface.

Base gives you (only) the ability to inherit implementation, not give a new implementation as an interface.
For a mixin, that's mixing-in. For a class, it's extending.

@leafpetersen
Copy link
Member Author

leafpetersen commented Dec 15, 2022

As for base taking away the implements capability, that's what base class does too.

I am adamantly opposed to having to explain these in terms of negative capabilities. It's too confusing. If I see base XXX I really want that to mean "thing which can be extended and also XXX".

I buy everything in the class (no mixin) rows.

In the mixin (no class) rows, I'm completely lost. I can't explain the third column to a user there.

In the mixin class rows, the difference between base mixin class and interface mixin class (and similarly with the abstract version) is confusing, and I don't understand it. base mixin class should mean extend, instantiate, mix-in because that what it says: it says "base" (I can extend), "mixin" (I can mixin) and "class" (I can instantiate): and that's what it means. So similarly interface mixin class should mean implement, instantiate, mix-in: but it doesn't.

@lrhn
Copy link
Member

lrhn commented Dec 15, 2022

It sounds like you read "mixin" as a capability. "If it says interface mixin, I can implement and mix-in."

Mixin is not a capability, it's a declaration. A plain mixin declaration declares a mixin, like a plain class declaration declares a class. A mixin is a different thing than a class.
A class can be extended and implemented, and certain modifiers can enable only some or none of those outside of the library, with base allowing extending.
A mixin can be mixed-in and implemented, and certain modifiers can enable only some or none of those outside of the library, with base allowing mixing-in
A mixin class declaration declares both. The modifiers affect both.

Generally, the base modifier allows inheriting implementation (extends for classes, with for mixins), it's not just "can extend".

@leafpetersen
Copy link
Member Author

@lrhn I think I see how you can come up with an internally consistent story using the model you describe, but it's terribly complex. "You see, we have three different things, class, mixin, and class mixin. And there's a modifier base which you can apply to any one of them, in which case it only has the capabilities associated with base. But the capabilities associated with base depend on what it's being applied to. If the thing has class in the name, then the capabilities include extend. If the thing has mixin in the name, then the capabilities include mix in. By implication, if the thing has class and mixin its name, it has both capabilities. All clear"?

Stepping back, what is the point of using using a class mixin instead of a mixin? As best I can tell, it gives you exactly two additional affordances:

  • You can construct it
  • You can extend it

For the latter, if we think supporting this combination is important, we could simply allow mixin declarations to be extended (with the usual Object with M semantics), and that eliminates any motivation there. That makes all of the abstract ... mixin class combinations redundant with a normal mixin declaration.

So the only remaining interesting cases are the ones that allow instantiation:

Declaration Inside library Outside library
mixin class extend, implement, instantiate, mix-in extend, implement, instantiate, mix-in
base mixin class extend, implement, instantiate, mix-in extend, instantiate, mix-in
interface mixin class extend, implement, instantiate, mix-in implement, instantiate
final mixin class extend, implement, instantiate. mix-in instantiate

Other than mixin class which we need for backwards compatibility, do we really feel that we have strong enough use cases for these to justify all of this? We could have a much simpler model which looks like:

Declaration Inside library Outside library
class extend, implement, instantiate extend, implement, instantiate
base class extend, implement, instantiate extend, instantiate
interface class extend, implement, instantiate implement, instantiate
final class extend, implement, instantiate instantiate
abstract class extend, implement extend, implement
abstract base class extend, implement extend
abstract interface class extend, implement implement
abstract final class extend, implement  
(abstract) sealed class extend, implement, exhaust exhaust
     
mixin extend, implement, mix-in extend, implement, mix-in
base mixin extend, implement, mix-in extend, mix-in
interface mixin extend, implement, mix-in implement
final mixin extend, implement, mix-in  
sealed mixin extend, implement, mix-in, exhaust exhaust
     
mixin class extend, implement, instantiate, mix-in extend, implement, instantiate, mix-in

I still don't love that base mixin behaves asymmetrically with interface mixin, but it's easier to swallow when it's just one odd case.

This is all assuming that we go with your model in which classes may not be mixed in even with their own library. If we do think that the cases that I've cut out of the table above are useful, then it seems to me we'd be much better of just saying that classes can be mixed in internally to a library no matter what, and then we end with most of the XXX class and XXX mixin class entries being identical. That is, we end up only needing XXX mixin class for things which result in mix in being available externally since all of the other use cases are covered. And if we allow regular mixin declarations to be extended, then abstract mixin class and abstract base mixin class become redundant with mixin and base mixin, leaving us with the following:

Declaration Inside library Outside library
class extend, implement, instantiate, mix-in extend, implement, instantiate
base class extend, implement, instantiate, mix-in extend, instantiate
interface class extend, implement, instantiate, mix-in implement, instantiate
final class extend, implement, instantiate, mix-in instantiate
abstract class extend, implement, mix-in extend, implement
abstract base class extend, implement, mix-in extend
abstract interface class extend, implement, mix-in implement
abstract final class extend, implement, mix-in  
(abstract) sealed class extend, implement, exhaust, mix-in exhaust
     
mixin extend, implement, mix-in extend, implement, mix-in
base mixin extend, implement, mix-in extend, mix-in
interface mixin extend, implement, mix-in implement
final mixin extend, implement, mix-in  
sealed mixin extend, implement, mix-in, exhaust exhaust
     
mixin class extend, implement, instantiate, mix-in extend, implement, instantiate, mix-in
base mixin class extend, implement, instantiate, mix-in extend, instantiate, mix-in

@lrhn
Copy link
Member

lrhn commented Dec 16, 2022

we could simply allow mixin declarations to be extended (with the usual Object with M semantics), and that eliminates any motivation there.

That's #1942. I've argued that it might confuse people to the difference between classes and mixins, but at this point, I'd do anything to disallow deriving mixins from arbitrary classes.
So, sure!

So the only remaining interesting cases are the ones that allow instantiation:

I could easily be convinced to not care about that at all.

I'm not aware of any class which is used both as a mixin, and to be instantiated. It makes very little sense, since a mixin is something you want to add to something else. It has no generative constructor, so it has almost no interesting state (it can do final now = DateTime.now();, but that's about as much as it can vary - reading global variables at creation.)
Creating an instance of it would be like having a Comparable with no values to compare.
I'd expect all such classes to be abstract.

So just doing #1942, not allowing mixin class at all, and telling people to migrate their mixin-classes to mixins (which can then be extended), would solve all the problems.

We could also say that only legacy code can extend mixins, and migration to 3.0 requires changing extends to with (which should be shorter in every case, especially if there are further mixins being applied).
On the other hand, allowing you to extend a mixin provides a migration path for someone who wants to change a class to a mixin, without actually costing anything. (And we could have a lint which tells you to use with for mixin applications, even if extends is allowed by the language.)

I still don't love that base mixin behaves asymmetrically with interface mixin,

I don't think it does. The capability of base is inheriting implementation, the capability of interface is implementing interface without implementation.
Sometimes you want to enforce that any subtype gets the implementation.
Sometimes you want to enforce that nobody else can get the implementation.
For a mixin, inheriting implementation means mixing in. The extends syntax with a mixin is just a shorthand for mixing in. You can treat "mix-in(extend)" as a single entry, with "extend" only available to mixins with no on clause.

@eernstg
Copy link
Member

eernstg commented Dec 16, 2022

I basically like all these proposals! 😸

But I do tend to prefer having mixin class rather than adopting #1942, for the reason @lrhn mentioned: It seems likely that the concept of a mixin and the concept of a class will be established more clearly if mixins can only be mixed in. Also, it does make sense to me to say that a mixin class declares a mixin and a class.

One point I'd like to make is that a list of 23 or 16 different keyword sequences looks more complex than it is.

We can demonstrate the underlying orthogonality by introducing an explicit separation of certain aspects of the declared entity. In short, a declaration which is relevant to this discussion has a set of kinds, a set of derivation capabilities, and an instantiation capability:

Kind
class
mixin
mixin class

Being a class implies a number of things (e.g., the ability to have members, possibly with an implementation), and similarly for a mixin. They both support implementation inheritance, but in different forms (subclassing vs. mixin application), but it is possible for a single declaration to support both kinds.

Next, we can specify some derivation affordances:

Derivation Meaning
final No capabilities, 00
base extends/with, 01
interface implements, 10
(empty) both, 11

Finally, we can specify the ability to have direct instances:

Instances Meaning
abstract no, 0
(empty) yes, 1

Note that all these properties except abstract are positive: mixin and class provide a mixin application capability vs. a subclassing capability; base turns on the 'can offer implementation inheritance' capability, and interface turns on the 'can deliver an interface that another class/mixin declaration can commit to' capability. The fact that the empty derivation aspect means 'all capabilities' is an exception, but it is useful with the given history.

Now we can basically use every combination of these aspects, and the meaning of that combination is determined directly by the meaning of its parts. We get 2 times 4 times 2 combinations using class or mixin class, plus 1 times 4 times 1 combinations using mixin (where abstract is implied and must be omitted), 20 in total.

This is exactly @lrhn's table here, except that sealed is omitted (with respect to these capabilities it is the same as abstract final).

I understand that we can make the list shorter, like @leafpetersen's last list here, which is 14 elements (plus sealed).

However, I don't think the 20 element list is so daunting when we take into account that it consists of an orthogonal combination of 3 clearly separate parts.

In summary, I think we should keep @lrhn's proposal here on the table.

@munificent
Copy link
Member

Wow, there is a lot of tables here. Let me respond to a couple of high level points and go from there.

Positive versus negative capabilities

One of the long-standing challenges with this feature has been finding intuitive keywords and I agree strongly that being able to use positive words and reason about them as positive capabilities is the strongest aspect of what we settled on (before mixin came into play).

Disallowing mixing in non-mixin classes within the same library

I think we should continue to allow any class declaration to be used as a mixin with the same library. It keeps the proposal more regular: All of these modifiers only affect what other libraries are able to do with the type. I think it's also more consistent with Dart: We say that every class and mixin declaration also gives you an interface for free, so it makes sense that a class also gives you a mixin for free when it makes sense to.

Also, pragmatically, I believe allowing you to mixin classes inside the same library makes our life a little simpler in terms of figuring out which set of combinations we need. Being maximally permissive inside the library means we don't need to worry about giving users a way to opt in to capabilities internally independently of exposing the same capability externally.

Mixin as a modifier

@lrhn's take that mixin class is another kind of declaration and not just a mixin capability modifier applied to a class resotates with me. I read it as mixin-class, like a single noun. (I admit it's a little confusing that interface class also reads like a noun but interface behaves more like a modifier.)

Kinds and capabilities

I really really like @eernst's breakdown where class, mixin, and mixin class each denote a kind of entity, then final/base/interface positively specify the subclassing and subinterfacing capabilities of that kind. Then abstract and sealed do their things.

The tables

I went through all of the rows in Leaf's table and Lasse's table and compared it to the most recent set of combinations in my PR. The only differences are that I allow some modifiers on mixin class while Leaf's table does not:

  • base mixin class
  • abstract mixin class
  • abstract base mixin class

And I disallow a couple of combinations that Lasse allows:

  • interface mixin class
  • final mixin class
  • abstract interface mixin class
  • abstract final mixin class
  • sealed mixin class

I believe the differences here are:

  • Leaf's table assumes we allow extending mixin declarations externally. Therefore there's no need to have base mixin class (a thing you can both extend and mixin) because you could express that with just base mixin and similarly for the other combinations he excludes.

  • Lasse's table assumes we disallow mixing in classes internally. Therefore you need mixin class for some combinations that don't expose any external capabilities that aren't already exposed by a different combination.

If you flip both of those assumptions:

  • As Erik suggests, we don't fix Allow mixins in "extends" clauses #1942 and keep requiring users to mix in mixins and don't allow extending mixin declarations.

  • As with the other restrictions which are ignored inside the library, we continue to allow you to mix in a class declaration inside its own library.

Then I think you end up with the table I propose. So I think we just need to reach agreement on these two points and the specific set of combinations will naturally flow from that.

Does that sound right?

@leafpetersen
Copy link
Member Author

Then I think you end up with the table I propose. So I think we just need to reach agreement on these two points and the specific set of combinations will naturally flow from that.

I'm basically fine with your proposal, but I continue to believe that the three additional entries that your proposal has over mine add very little value. Specifically:

  • base mixin class
    • Arguably the most useful. Allows construction and extension, which base mixin does not. But... extension is not really necessary, you can just do Object with M instead`, and construction is of very limited use: you can only have one of these things if there is no non-default generative constructor.
  • abstract mixin class
    • Not very useful. Just use mixin instead, and use Object with M if you want to extend.
  • abstract base mixin class
    • Not very useful. Just use base mixin instead, and use Object with M if you want to extend.

So while I'm ok with having these, I think they add relatively minimal value.

@lrhn
Copy link
Member

lrhn commented Dec 16, 2022

I agree that some combinations are not particularly useful, so the question is whether we go for full orthogonality or not.
If we do, some combinations might never be used, but if they are, then the semantics are clear.

If we don't, we need to determine which are not useful enough, which of the equivalent forms is the one we'll approve of, and make our tools recognize the rest, and probably suggest what you should change them to. ("You wrote abstract base mixin class, so chang it to base mixin and change extends to with".)

My only goal with introducing mixin class is to prevent mixing in classes not intended for mixing in.
Making a declaration be both would satisfy all the current use-cases, because that's effectively what they do.
If we can prevent mixing in arbitrary classes in other ways, #1942 or otherwise, I'd still be happy.
If we have to allow legacy code to ignore that restriction, I'm also fine. It'll go away with time.

(I still don't see the benefit of allowing mixing in a class inside the same library. Just declare it as a mixin. If we allow you to mix in classes inside the same library, we can also slacken the rule about using classes with super-classes as mixins. After all, you can see all mixin applications in the same library, so you can eagerly check that the super-invocations are valid.)

@munificent munificent changed the title [Capability Modifiers] Adding mixin confuses positive and negative capabilities again [Class Modifiers] Adding mixin confuses positive and negative capabilities again Dec 17, 2022
@munificent
Copy link
Member

munificent commented Dec 17, 2022

My general reasoning for prohibiting a combination is either:

  • It's redundant. I don't want to give users two ways to say the same thing because they will assume that the distinction matters when it doesn't and then get confused looking for a difference. For example, combining sealed and abstract.
  • It's conflicting. If the two modifiers each express an opposing intent, it's not even clear what the users means when they combine them. To avoid that, I rule it out. For example, mixin class means "I want this class to be used as a mixin" but sealed means "I don't want this class to be used as a mixin", so we prohibit sealed mixin class. Likewise interface ("this class can be implemented") and sealed ("this class can't be implemented"), etc.

That does leave combinations that aren't particularly useful. But I left those in because... who knows? Maybe someone will come up with a use for them? We tend to be optimistic in terms of making features be as general as possible even if we don't have compelling use cases in hand.

I don't know if someone will find a use for abstract mixin class or not. Heck, I don't know if sealed mixin is even really useful in practice. But as far as I can, the meaning of those combinations is coherent and non-harmful, so I left them in.

That being said, I'm certainly not strongly attached to them, and if you'd rather us be a little more prescriptive and leave these out, I'm fine with that too. I expect most combinations will be very rarely used in practice except for sealed class, final class, interface class, and base class. So I'm not too worried about the other combinations one way or the other. My main concern is that every combination we ship must have well defined coherent semantics.

@leafpetersen
Copy link
Member Author

That does leave combinations that aren't particularly useful. But I left those in because... who knows? Maybe someone will come up with a use for them? We tend to be optimistic in terms of making features be as general as possible even if we don't have compelling use cases in hand.

I'm fine with this. My main objection was/is to things like interface mixin class, which required the user to think negatively: this is not a class which can be used as an interface or as a mixin; it is a class which cannot be extended or mixed in, despite having the mixinmodifier.

@lrhn
Copy link
Member

lrhn commented Dec 17, 2022

For example, mixin class means "I want this class to be used as a mixin" but sealed means "I don't want this class to be used as a mixin"

I'll oppose the interpretation of mixin class as a "class that I want to be used as a mixin". It's a class and a mixin.
It can be extended and mixed in. A non-class cannot be extended. A non-mixin cannot be mixed in.
The sealed says that I don't want other people to do so.
Just as sealed class is something you can extend, but others cannot. It's not "something I intend to be extended, but then sealed says you can't".

Again, that only makes sense if you cannot mix in a non-mixin class yourself, and I think you shouldn't be allowed to.

As Erik put it:

  • mixin and class introduce capabilities ("extend" and "instantiate" for class, "mix in" for mixin, "implement" for both/either). These are both positive. If they are not there, you don't have those capabilities. A mixin class have all of them.
  • base, interface, final and sealed are restrictions on what other libraries can do with those capabilities, but the current library is not affected. Both interface and final prevents inheriting implementation, which blocks the "extend" and "mix in" capabilities, while base and final block the "implement" capability, and sealed is like abstract final + exhaustiveness.
  • abstract is a restriction on "instantiate" for everybody, because it allows the class to be incomplete and impossible to instantiate.

A sealed mixin class is something that you can extend, mix in and implement, but others cannot. That's not contradicting that you say that it's intended to be mixed in. It's just also saying that it only applies to you.

@munificent
Copy link
Member

Closing this because we've settled on the keywords.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

4 participants