gbracha / lessRestrictedMixins

Lift some, but not all, of the current restrictions on mixins in Dart.
Apache License 2.0
2 stars 2 forks source link

replace `extends` syntax to avoid confusion #2

Open sigmundch opened 9 years ago

sigmundch commented 9 years ago

I was expecting that when removing the second restriction (that mixins can extend other classes), that the mixin application would include the code from both the mixin class declaration and its superclasses.

Let me use this code as an example:

class Base {
  get f => 1;
}

class M1 {
  get f => 2;
}

class M2 extends M1 {
  get f => super.f;
}

class Test1 extends Base with M2 {
}

Under the current proposal, what happens when evaluating new Test1().f?

If I understand things correctly, it seems that this would result in the value 1. That is because Sdynamic resolves to Base, not M1. Also, it seems that we'd produce a warning according to:

Let A be an application of MA. It is a static warning if the superclass of A is not a subtype of S.

because, even though Base is structurally like M1 it is not marked as a subtype of M1. Is that correct?

I was hoping that this code would not produce a warning, and would result in the value 2. In a way, equivalent to having the user write:

class Test2 extends Base with M1, M2 {
}

The use case for this, is that we could have lots of classes that can be mixed in, and easily provide a mixin that users can apply to get a combination of those classes. For example:

class PolymerMini extends Object with AttributesFeature{ }
class PolymerAll extends PolymerMini with BindingFeature, TemplateFeature, RandomFeature {}
...
// A user simple mixes the combo of features he desires:
class MyElement extends HtmlElement with PolymerAll { ... }

Backing up a bit: it seems to me that there are 2 possible meanings we could give to the extends M1 clause in a mixin.

Not sure if we can have both with the same syntax. The current syntax makes me think more of the former, so maybe we could have a separate syntax for the latter?

Here are a couple a random and not that exciting ideas, where we would encode the current semantics that only the code in M2 is applied in the mixin, and we require the base class to implement M1:

// new keyword "within"
abstract class M2 within M1 {
  get f => super.f;
}

// use an artificial "super" getter
abstract class M2 {
  M1 get super; // declares that M2 should be applied in the context of an M1
  get f => super.f;
}
gbracha commented 9 years ago

I think what you are thinking of is mixin composition: the ability to name and reuse a chain of mixin applications. Mixin composition is actually discussed in the spec as a semantic concept, but there is no syntax for it.

Mixin composition is entirely analogous to function composition. One way to think of a mixin is as a function from superclasses to subclasses. Apply the mixin/function M to a given superclass S, and you get a new subclass of S. The function is defined by what goes inside the curly braces of a class declaration. This is the basic concept; once you have it, you can define mixin composition b analogy with function composition, so you can define an abstraction that takes a superclass, and produces a new subclass that includes the contributions of a series of intermediate classes.

It is best to have the primitive available first, and build upon it later. So I see adding mixin composition as a separate DEP.

sigmundch commented 9 years ago

Yes, and I agree I can see composition being it's own separate DEP. However, I do wonder if we should use a different syntax for this DEP in preparation for it.

In particular, I fear that the extends syntax is an intuitive fit for composition. Many users will naturally conclude that they get mixin composition whenever they see a mixin application containing a class with a superclass. For example, I think it's natural to think that A1, A2, and A3 below will have the same behavior:

class C { ... }
class D { ... }
class CD1 extends Object with C, D {}
class CD2 extends C with D { ... }
A1 extends B with C, D {}
A2 extends B with CD1 {}
A3 extends B with CD2 {}

One reason for this, I think, is that everywhere else in Dart where we use extends or implements we expect the transitive set of classes in that relation. For instance, A implements B means that A also implements anything B extends or implements too. So it's intuitive to extrapolate and say that A extends Object with M should be equivalent to A extends M, regardless of whether M has a base class or not.

eernstg commented 9 years ago

One possible approach (the gbeta appreach by the way, of course) would be to let

class Test1 extends Base with M2 {
}```

mean "for all the mixins that make up class_M2 (the list [mixin_M1, mixin_M2]), starting with the most general one (mixin_M1), apply them to Base, finally add the literal mixin `{..}`". This means that a mixin declaration where the superclass is a proper subclass of Object is a declaration of a list of mixins. An application of such a list of mixins will add the ones that are missing from the superclass that the mixin list is applied to.

When all the missing mixins from class_M2 get added to Base, all the inherited method implementations that mixin_M2 expects will be present in Test1 such that `super` will have an implementation to select.
gbracha commented 9 years ago

We introduced the 'with' keyword precisely because it doesn't do what 'extends' does. It is a different syntax. It is really the mixin application operator.

sigmundch commented 9 years ago

Adding @munificent, he mentioned he also has been thinking about the same concern I have.

Two additional ideas:

  1. another keyword similar to within: requires
  2. introduce a new speudovariable, like super, called supermixin?

The idea behind 1 was to declare what you expect to have in your type-hierarchy chain. But I realize that it may add some confusion as to what the target of the super call should be. In particular, consider this example example:

class B {
  foo() => print('1');
}
class C {
  foo() => print('2');
}
class M requires B {
  foo() => super.foo(); 
} 
class Example extends C with M {}

Example.foo resolves to M.foo, Should M.foo call B.foo or C.foo?

Under the current proposal, I believe the current answer is C.foo. But just to be sure.

The idea behind 2 is to avoid using extends or adding a new keyword for this. However, we add a way to distinguish calls in the context of inheritance vs mixin application (so that we continue giving warnings for super.foo as we do today in the language).

For example:

class M {
  // warning if M is mixed in a context where the super class doesn't have `foo`.
  foo() => supermixin.foo(); 
}

I personally prefer option 1 because mixin applications do have the practical result of creating a concrete class hierarchy (with anonymous classes that represent each step of a mixin application), so the super keyword does make sense in that context.

zoechi commented 9 years ago

Looks like your example with the M, B, and C classes misses something. C is not used.

sigmundch commented 9 years ago

Ah, good point - I forgot to add the class with the mixin application. I edited the comment above to include class Example extends C with M

munificent commented 9 years ago

I agree with Siggy that users will expect mixin composition to be available in some way. However, I don't know if I feel that a class declaration should implicitly define both a mixin and a mixin composition with its superclasses and that users can then choose which one they want.

My intuition (which could be wrong) is that it's fine for a class declaration to always just define a concrete class and a mixin.

For a mixin composition, maybe we could extend typedef to let users bind a name to a mixin composition? Something like:

class Quack {
  void quack() { ... }
}

class Swim {
  void swim() { ... }
}

typedef Waterfowl with Quack, Swim;
// Or some other less bad syntax...

class Duck extends Bird with Waterfowl {}

main() {
  var duck = new Duck();
  duck.quack();
  duck.swim();
}

An open question is whether the typedef also defines a concrete class. In other words, what would it mean to do new Waterfowl() in this example? The simplest answer I think is to say that it defines an abstract class with an empty body. That, I believe, has the behavior users expect.

Thoughts?

sigmundch commented 9 years ago

My intuition (which could be wrong) is that it's fine for a class declaration to always just define a concrete class and a mixin.

This is very interesting because I always thought that they are both the same, and that the reason mixins can only extend from Object today was because by adding extends we would add mixin composition.

Thoughts?

I like your proposal for mixin composition, I just worry that extends suggests the wrong idea. I wonder how easily we can help people to move away from my intuition, and explain the intuition of the class vs mixin being different.

One point where I think this becomes relevant is on error messages, since that's where a lot of people will learn what's right or wrong with their code. Maybe we can look at concrete examples and see what error messages could look like?

Here is an initial example:

class A extends B { ... }
class C extends Object with A {... }
                       ^^^^^^ // error

What should the error message be here? One idea: "Cannot mix 'A' in this context. 'A' expects that 'B' is implemented by the superclass". Is this good enough?

I'd love if we could keep looking at additional error-cases and see if we can build that intuition easily.

jakemac53 commented 9 years ago

My intuition (which could be wrong) is that it's fine for a class declaration to always just define a concrete class and a mixin.

This is very interesting because I always thought that they are both the same, and that the reason mixins can only extend from Object today was because by adding extends we would add mixin composition.

I definitely have the same intuition as @sigmundch , if I see extends that definitely implies composition to me. I would rather have a new keyword such as requires which is clearly different.

typedef Waterfowl with Quack, Swim;

Something like this could be interesting, but I am curious what that would translate to in this case:

class Animal {}
class Language {}
class Legs {}
typedef SpeakingAnimal with Animal, Language;
typedef WalkingAnimal with Animal, Legs;
class SpeakingWalkingAnimal extends Object with SpeakingAnimal, WalkingAnimal {}

Is that equivalent to:

class SpeakingWalkingAnimal extends Object with Animal, Language, Animal, Legs {}

If so, what are the possible implications of mixing in the same class (Animal) more than once? This is a general issue with composition it seems like :).

gbracha commented 9 years ago

The reason for the existing restriction was nothing to do with mixin composition. It had to do with dealing with constructors, and with implementation considerations. By separating out the constructor issue, we can move forward while preserving the basic model, which is what makes mixin composition possible.

I find the typedef syntax difficult to understand; I'd expect an equal sign or something. I also think that using 'with' here is a conceptual error, because 'with' is our syntax for mixin application. Mixin composition is a different operation, just as function application and composition are different (this is not a loose analogy, but a very precise one BTW). It's the comma that acts as composition, so I would define

typedef WalkingAnimal = Animal, Legs;

At which point the expansion you give is correct. Repeated mixin application is exactly like repeated function application. It introduces a new class into the inheritance chain. Super calls bind to the next class up the chain, so the two uses of Animal may indeed differ. There is a temptation to ask for magic like removing repeats, at which point compositionality goes out the window.

sigmundch commented 9 years ago

Another idea for a different keyword under:

class A under B {}
class C extends Object with A {}

produces:

warning: 'A' can only be mixed-in in a context where the base class
implements 'B', but 'Object' does not implement 'B'.

    class C extends Object with A {}
                    ^^^^^^