dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.29k stars 1.59k forks source link

mixin member invalid override on non conflicting types error #57005

Open bar4488 opened 4 weeks ago

bar4488 commented 4 weeks ago

It seems that if 2 mixins declare the same member with different bounds, the type of the latter mixin member must conform to the type first mixin member, which can throw an error even in cases where the class using the mixin is able to satisfy both types.

mixin LongRangeWeapon {}

mixin ExplosionWeapon {}

class Shotgun with LongRangeWeapon, ExplosionWeapon {}

mixin Shooter {
  LongRangeWeapon get weapon;
}

mixin Bomber {
  ExplosionWeapon get weapon;
}

abstract class Combatant with Shooter, Bomber { // 'Bomber.weapon' ('ExplosionWeapon Function()') isn't a valid override of 'Shooter.weapon' ('LongRangeWeapon Function()')
  @override
  Shotgun get weapon;
}

expected behavior: There should not be any error as long as the class itself validly satisfies both mixin.

dart-github-bot commented 4 weeks ago

Summary: The issue arises when two mixins declare the same member with different bounds. The type of the latter mixin member must conform to the type of the first mixin member, even if the class using the mixin can satisfy both types. This leads to an invalid override error, even when the class provides a valid implementation.

lrhn commented 4 weeks ago

The problem being reported is that the class Object with Shooter, Bomber (the superclass of Combatant) has an invalid override.

The class Object with Shooter has a member LongRangeWeapon get weapon;. The class Object with Shooter, Bomber has that class as superclass, and then declares a member with signature ExplosionWeapon get weapon;

Since ExplosionWeapon get weapon; is not a valid override of LongRangeWeapon get weapon;, that class is invalid. That class exists! It might be abstract, anonymous and generally inaccessible (other than through super calls inside Combatant), but the language doesn't care. The class exists and has to be valid. Also because the interface of that class is the interface that Combatant inherits.

If you need to inherit multiple independent abstract declaration, it's better to use interfaces. Those are all added to the same class, and can be overridden by that classs. Mixins are applied sequentially, and all the intermediate classes must be valid.

bar4488 commented 4 weeks ago

I see, it would be really great if we could support such use-cases. This patten seems quite common in order to describe complicated traits tree.

It seems that a support for extended types would solve this problem, and also allow much more flexibility to the type system:

class A{}
mixin B on A{}
mixin C on A{}

typedef D = A with B,C

This would also allow for a class to declare members with type D, thus solving the problem stated in the issue.

Is there any issue open to support such a feature?

lrhn commented 3 weeks ago

Not sure what such extended types world be. You can write

class D = A with B, C;

today, but it has the same issue because it does introduce the intermediate mixin application classes too.

I am not aware of any proposal that would remember this issue.

Such a feature would change how mixin application works, either by removing some normal class constraints from the anonymous mixin application classes, or by collapsing mixin applications into a single class (like being augmentations instead of separate classes, but then not having the same constraints as augmentations).

Maybe the solution is to use interfaces to drive the combined types, and only use mixins for the final implantation, with no two mixins containing the same member, not even as abstract. Or using generics for anything that differs between subclasses.

mixin LongRangeWeapon {}

mixin ExplosionWeapon {}

class Shotgun with LongRangeWeapon, ExplosionWeapon {}

mixin Shooter<W extend LongRangeWeapon> {
  W get weapon;
}

mixin Bomber<W extends ExplosionWeapon> {
  W get weapon;
}

abstract class Combatant<W extends Shotgun> with Shooter<W>, Bomber<W> {
  @override
  W get weapon;
}

// Or, of Combatant will not be extended
abstract class Combatant with Shooter<Shotgun>, Bomber<Shotgun> {
  @override
  Shotgun get weapon;
}