dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

Allow mixins to declare factory constructors #1803

Open munificent opened 3 years ago

munificent commented 3 years ago

Mixins are almost always superior to interfaces. In particular, they allow the mixin author to add new methods (with implementations) without breaking downstream consumers. With an implements clause, any change to the superinterface is a breaking change.

One capability that abstract-classes-as-interfaces do have is that they can declare constructors (which are often factories). That lets you define a type who name refers to both the thing you can implement and a way to construct instances of some canonical implementation type.

Mixins don't allow that. It doesn't make sense to allow mixins to define generative constructors, but there's nothing semantically troublesome about allowing them to define factory constructors. Mixins can already define static members, and factory constructors are essentially the same.

This would give users a more flexible way to define interface-like types that can be evolved in non-breaking ways and be directly constructed.

lrhn commented 3 years ago

I believe we allow mixins to be derived from classes with a factory constructor, so it's probably an oversight (or an attempt to minimize the feature) that mixin declarations do not allow factory constructors.

Most mixin declarations are defined in terms of how they embellish or implement other types, and a factory constructor for those don't make that much sense (to me, at least). However, you can create a mixin intended as either a base implementation of an interface, which can be used either as a base class (directly on Object) or applied to another class. Such a mixin can meaningfully have a factory constructor.

So, no problem allowing it, and seeing a potential use-case.

If we want to push for mixins being used more as a way to introduce new members to an interface in a non-breaking way, instead of, e.g., interface default methods (#884), this can also be a step on that way.

munificent commented 3 years ago

If we want to push for mixins being used more as a way to introduce new members to an interface in a non-breaking way, instead of, e.g., interface default methods (#884), this can also be a step on that way.

I do. Not because I'm morally opposed to interface default methods. But, I mean, we've already got mixins. May as well get as much value out of them as we can. :)

lrhn commented 3 years ago

If we could unify the two in some way, that would be nice, but asking people to change all their implements to with is a ship long sailed. I really do want the functionality interface default methods, allowing you to add a method to an interface without breaking existing implementations, and the current mixins is not a usable replacement for that.

munificent commented 3 years ago

If we could unify the two in some way, that would be nice, but asking people to change all their implements to with is a ship long sailed.

I don't think we need to migrate the entire world to this pattern. It's more that new code should probably consider using mixins instead of interfaces. Existing packages, when they want to make a breaking change, may want to take that opportunity to switch some interfaces to mixins.

kasperpeulen commented 3 years ago

You can also add a method to an interface using an extension method in a non breaking way.

I think that should actually most often be the preferred solution.

Making a clear seperation between what it means to be an “interface” and what you get (for free) by being that interface.

bernaferrari commented 3 years ago

Whatever you do, please make a lint that does this (to help in migration even if not 'needed') 😛

munificent commented 3 years ago

You can also add a method to an interface using an extension method in a non breaking way.

I think that should actually most often be the preferred solution.

That has its own set of trade-offs:

If you do own the interface, I think it's generally better to make members be instance members when you can. The "extensions on my own type" pattern is necessary for some things (like enums), but it makes me sad and I think makes the API a little more confusing.

eernstg commented 3 years ago

We're likely to support extends M where M is a mixin as syntactic sugar for extends Object with M. This is intended to allow maintainers of a class which is currently used as a mixin to change it to a mixin declaration.

However, existing code could also create an instance of such a class (it could not have an explicitly declared generative constructor, but it could have an implicitly induced one, or it could have some factory constructors). This means that a smooth migration might require the ability to declare a factory constructor in a mixin.

I think this serves as an argument in favor of supporting factory constructors in a mixin declaration, that is, to do what's proposed in this issue.

About the conceptual side: I think it's reasonable to expect a mixin used as an interface (as in implements M) to work like any other interface. It typically includes an implementation of some methods which can be "enabled" by using with M rather than implements M, but in the general case a class may very well need to have the interface of M without being able to use the method implementations in M, so I don't see it as a goal to replace implements M by with M, or as a code smell to have implements M in the first place (and, with the new feature, extends M and new M()).

The remaining question is perhaps: Why don't we get rid of abstract classes? ;-)

lrhn commented 3 years ago

If you could only mix-in (with) a mixin, not implement it, we'd have semi-sealed interfaces with guaranteed implementation of some methods. Nah, not really, you would just do class MyMixin = Object with Mixin; and then implement MyMixin. Unless that class also couldn't be implemented, but then we might as well introduce no-interface classes and mixins if that's the way we want to go.

As long as we can extract interfaces from classes, we should also be able to extract them from mixins, and then mixins should have factory constructors as well.

goderbauer commented 2 years ago

Here is a (small) use case where I wish I could define a factory constructor for a mixin: https://github.com/flutter/flutter/issues/101386#issuecomment-1089438437

ItsCubeTime commented 1 year ago

I also find this important. It's one of the primary reasons that I don't use Dart at this date as a significant amount of code that I write requires magical methods from classes inherited from multiple lanes - often where some classes inherited are from external APIs making it impossible for me to handle everything under a single metaclass.

Having to split data into several classes/objects/structures can add a significant amount of indirectness to the code flow & reduce readability