Proposal : rest instance variables
xmehaut opened this issue · 27 comments
In order to implement in a simple manner mixins, it could be interesting to provide a kind of rest mechanism for instances variables (and methods).
Imagine the following syntax :
class OtherClass {
dummy1 : string;
dummy2 : number;
dummy() {
}
}
class MyGenericClass <aClass> {
...: aClass;
}
class MyTerminalClass extends MyGenericClass<OtherClass> {
aMethod() {
let toto = this.dummy1;
let titi = this.dummy2;
this.dummy();
}
}
This could be a nice way to define and use mixins...
We may also combine mixins like this :
... : OtherClass & OtherOtherClass ;
how is that diffrent from:
class MyGenericClass <aClass> extends aClass {
}It is different because we expans in a class the content of other(s) classes, ie like mixins do, enabling in a certain manner multiple inheritance we don t have in modern languages , we had in first Smalktalk versions.
Envoyé de mon iPhone
Le 16 janv. 2016 à 01:06, Mohamed Hegazy notifications@github.com a écrit :
how is that diffrent from:
class MyGenericClass extends aClass {
}
—
Reply to this email directly or view it on GitHub.
One example of use :
Imagine you have two react ui components like container and paper.
We could also have a third one which is containerpaper which fuses the features of the two previous ones. Instead of creating this third component, we could inject container capabilities into paper (or the contrary).
Envoyé de mon iPhone
Le 16 janv. 2016 à 01:06, Mohamed Hegazy notifications@github.com a écrit :
how is that diffrent from:
class MyGenericClass extends aClass {
}
—
Reply to this email directly or view it on GitHub.
I think the same effect could be achieved by having more freedom in the "extend from expression" feature. It is almost possible today with this syntax:
class X extends Mixin<A,B>() {
}And I would very much prefer that syntax over the ...: A; ...: B proposed.
Now, it seems to me that this is impossible in TS 1.7 because when I try that:
function Mixin<T,U>(t: new() => T, u: new() => U): new() => T & U { ... }the TS compiler complaints that "Base constructor return type 'T & U' is not a class or an interface.".
This is a little sad and if the restriction might be lifted somehow you would have nice Mixins in the langage with a single helper function. Is there (or could there be) a way to say that T and U generics parameters are constrained to classes and interfaces, so T & U is OK for the compiler?
You can make it work today if you accept creating one interface for each of your mixins combinations, like so:
// Your global Mixin helper function
function Mixin<Target, Src1, Src2>(a: new() => Src1, b: new() => Src2) : new() => Target { ... }
// Sample mixins you want to use
class Dog {
bark() {...}
}
class Duck {
quack() {...}
}
// This is the extra interface that you need, it's empty: just a one-liner
interface DoggyDuck extends Mixin1, Mixin2 { }
// Now you can easily create derived classes
class Mutant extends Mixin<DoggyDuck, Dog, Duck>(Dog, Duck) { ... }
// It works well at compile time and run time!
var xavier = new Mutant();
xavier.bark();
xavier.quack();And I think with TS 1.8 F-Bounded Polymorphism we might get extra safety allowing a constraint that Target extends Src1 & Src2.
Thanks jod for this nice piece of code, nevertheless a little bit tricky :)
Nevertheless, multi inheritance have some drawbacks we must face on like what about same methods defined in the different classes to be "merged", or same instance variables initialized differently? Except these cases, mixins are good means to avoid redefining some components when we need to have a tomato which is either a fruit and a vegetable :)
Personnally, i like the syntactic sugar i proposed :
... : A&B&C
It is quite concise and auto explained
Envoyé de mon iPhone
Le 19 janv. 2016 à 00:57, jods notifications@github.com a écrit :
I think the same effect could be achieved by having more freedom in the "extend from expression" feature. It is almost possible today with this syntax:
class X extends Mixin<A,B>() {
}
And I would very much prefer that syntax over the ...: A; ...: B proposed.Now, it seems to me that this is impossible in TS 1.7 because when I try that:
function Mixin<T,U>(t: new() => T, u: new() => U): new() => T & U { ... }
the TS compiler complaints that "Base constructor return type 'T & U' is not a class or an interface.".
This is a little sad and if the restriction might be lifted somehow you would have nice Mixins in the langage with a single helper function. Is there (or could there be) a way to say that T and U generics parameters are constrained to classes and interfaces, so T & U is OK for the compiler?You can make it work today if you accept creating one interface for each of your mixins combinations, like so:
// Your global Mixin helper function
function Mixin<Target, Src1, Src2>(a: new() => Src1, b: new() => Src2) : new() => Target { ... }// Sample mixins you want to use
class Dog {
bark() {...}
}
class Duck {
quack() {...}
}// This is the extra interface that you need, it's empty: just a one-liner
interface DoggyDuck extends Mixin1, Mixin2 { }// Now you can easily create derived classes
class Mutant extends Mixin<DoggyDuck, Dog, Duck>(Dog, Duck) { ... }// It works well at compile time and run time!
var xavier = new Mutant();
xavier.bark();
xavier.quack();
And I think with TS 1.8 F-Bounded Polymorphism we might get extra safety allowing a constraint that Target extends Src1 & Src2.—
Reply to this email directly or view it on GitHub.
Hi jods
Is there a means to do the same by using decorators to inject the mixin classes into the current one?
Envoyé de mon iPhone
Le 19 janv. 2016 à 00:57, jods notifications@github.com a écrit :
I think the same effect could be achieved by having more freedom in the "extend from expression" feature. It is almost possible today with this syntax:
class X extends Mixin<A,B>() {
}
And I would very much prefer that syntax over the ...: A; ...: B proposed.Now, it seems to me that this is impossible in TS 1.7 because when I try that:
function Mixin<T,U>(t: new() => T, u: new() => U): new() => T & U { ... }
the TS compiler complaints that "Base constructor return type 'T & U' is not a class or an interface.".
This is a little sad and if the restriction might be lifted somehow you would have nice Mixins in the langage with a single helper function. Is there (or could there be) a way to say that T and U generics parameters are constrained to classes and interfaces, so T & U is OK for the compiler?You can make it work today if you accept creating one interface for each of your mixins combinations, like so:
// Your global Mixin helper function
function Mixin<Target, Src1, Src2>(a: new() => Src1, b: new() => Src2) : new() => Target { ... }// Sample mixins you want to use
class Dog {
bark() {...}
}
class Duck {
quack() {...}
}// This is the extra interface that you need, it's empty: just a one-liner
interface DoggyDuck extends Mixin1, Mixin2 { }// Now you can easily create derived classes
class Mutant extends Mixin<DoggyDuck, Dog, Duck>(Dog, Duck) { ... }// It works well at compile time and run time!
var xavier = new Mutant();
xavier.bark();
xavier.quack();
And I think with TS 1.8 F-Bounded Polymorphism we might get extra safety allowing a constraint that Target extends Src1 & Src2.—
Reply to this email directly or view it on GitHub.
@xmehaut
I'm pretty sure you can inject your mixins at runtime using decorators, which is a nice syntax for that.
But I don't think there is a way to augment the static, compile-time type with decorators, which is a huge drawback: all your mixins have to be handled as any types :(
It is different because we expans in a class the content of other(s) classes, ie like mixins do,
so who is responsible for mixing in the new class at runtime? do you expect the generated code to do this for you? or you will be calling some extends method later on?
if it is the erlieri do not see how generrics pattern can ever be used here, and you should probally look into #2919 instead.
The generics into the example are not related with the expansion. We could without by just writing ... : type1 & type 2
Yes i would intend that the expansion would generate the js mixin code
Envoyé de mon iPhone
Le 19 janv. 2016 à 20:18, Mohamed Hegazy notifications@github.com a écrit :
It is different because we expans in a class the content of other(s) classes, ie like mixins do,
so who is responsible for mixing in the new class at runtime? do you expect the generated code to do this for you? or you will be calling some extends method later on?
if it is the erlieri do not see how generrics pattern can ever be used here, and you should probally look into #2919 instead.—
Reply to this email directly or view it on GitHub.
it is the same purpose , yes, and the syntax is a little bit different
beacuse they refer more to rest desconstructing than to inheritance-like
formalism... It is imprtant in the sense that we don't want to mimic
multinheritance, but we want more a copy-like behavior... For instance, if
we have in one of a mixin a method which has the same signature than the
one of the target class, it won't be copied and not overwrite the one in
place; the same for constructor or instance variables...
2016-01-19 20:34 GMT+01:00 Mohamed Hegazy notifications@github.com:
how is this different from #2919
#2919 or #311
#311? is it just the
syntax?—
Reply to this email directly or view it on GitHub
#6502 (comment)
.
You can make it work today if you accept creating one interface for each of your mixins combinations, like so:
Can you provide a working example on http://www.typescriptlang.org/Playground ?
@Izhaki Something along those lines. Note that I simplified the Mixin signature a bit.
function Mixin<T>(...mixins) : new() => T {
class X {}
Object.assign(X.prototype, ...mixins.map(m => m.prototype));
return <any>X;
}
abstract class Dog {
bark() { console.log('bark!'); }
}
abstract class Duck {
quack() { console.log('quack!'); }
}
interface DoggyDuck extends Dog, Duck { }
class Mutant extends Mixin<DoggyDuck>(Dog, Duck) {
}
let xavier = new Mutant();
xavier.bark();
xavier.quack();@jods4 This is outright genius!
I've taken the liberty to:
- Synthesise your
Mixintemplate with the one in the handbook, so:- It distinguishes between the base class and mixins
- It doesn't require
Object.assign
- Extend the example to show a normal class hierarchy.
I suspect Mixin() can be further improved, but this works:
function Mixin<T>( baseClass: any, ...mixins ) : new() => T {
mixins.forEach( mixin => {
Object.getOwnPropertyNames( mixin.prototype ).forEach( name => {
baseClass.prototype[ name ] = mixin.prototype[ name ];
});
});
return baseClass;
}
// The mixin
abstract class Painter {
paint() { console.log( 'paint' )}
}
// Class hierarchy Animal <-- Mammal <-- Human
class Animal {
constructor() { console.log( 'Animal constructor' ); }
eat() { console.log( 'Animal eats' ); }
}
interface AnimalPainter extends Animal, Painter {}
class Mammal extends Mixin<AnimalPainter>( Animal, Painter ) {
constructor() {
super();
console.log( 'Mammal constructor' );
}
eat() {
super.eat();
console.log( 'Mammal eats' );
}
}
class Human extends Mammal {
constructor() {
super();
console.log( 'Human constructor' );
}
eat() {
super.eat();
console.log( 'Human eats' );
this.paint();
}
}
let iHuman = new Human();
iHuman.eat();@mhegazy, I reckon this is much better than the current mixin strategy in the handbook, as it removes the redeclaration issue. Your thoughts?
very nice!
2016-01-20 16:25 GMT+01:00 Izhaki notifications@github.com:
@jods4 https://github.com/jods4 This is outright genius!
I've taken the liberty to:
- Synthesise your Mixin template with the one in the handbook, so:
- It distinguishes between the base class and mixins
- It doesn't require Object.assign
- Extend the example to show a normal class hierarchy.
I suspect Mixin() can be further improved, but this works:
Playground
function Mixin( baseClass: any, ...mixins ) : new() => T {
mixins.forEach( mixin => {
Object.getOwnPropertyNames( mixin.prototype ).forEach( name => {
baseClass.prototype[ name ] = mixin.prototype[ name ];
});
});
return baseClass;
}// The mixin
abstract class Painter {
paint() { console.log( 'paint' )}
}// Class hierarchy Animal <-- Mammal <-- Human
class Animal {
constructor() { console.log( 'Animal constructor' ); }
eat() { console.log( 'Animal eats' ); }
}interface AnimalPainter extends Animal, Painter {}
class Mammal extends Mixin( Animal, Painter ) {
constructor() {
super();
console.log( 'Mammal constructor' );
}eat() { super.eat(); console.log( 'Mammal eats' ); }}
class Human extends Mammal {
constructor() {
super();
console.log( 'Human constructor' );
}eat() { super.eat(); console.log( 'Human eats' ); this.paint(); }}
let iHuman = new Human();
iHuman.eat();@mhegazy https://github.com/mhegazy, I reckon this is much better than
the current mixin strategy in the handbook, as it removes the redeclaration
issue. Your thoughts?—
Reply to this email directly or view it on GitHub
#6502 (comment)
.
@Izhaki No problem with the changes, my code was a quick proof of concept ;)
Just one point:
baseClass.prototype[ name ] = mixin.prototype[ name ]
Aren't you mutating your base classe ctor?
I think you need to create a new intermediate base class (like I did with X).
Beat me to it, but it works. I initially had a condition for 'constructor', but it works without that condition. It doesn't seem to override the base class one (although the mixin does have a constructor own property).
If you look at the code emitted, in extend, there's object.create(b) which means that the actual constructor should be the right one (the actual constructor is not the prototype property, but the 'private' javascript prototype.
@Izhaki I am not sure what you mean by "it works".
Starting with your code I can do this:
let bug = new Animal();
bug['paint']();This should crash but it does print "paint" in the console. This demonstrates that you have clearly modified Animal.prototype, which is bad.
My original code worked around this by copying all prototypes into a new one (the X class). The only drawback is that the base ctor was not called but this is easily fixable. Here is a better mixin function:
function Mixin<T>(...mixins) : new() => T {
class X {
constructor() {
mixins[0].call(this);
}
}
Object.assign(X.prototype, ...mixins.map(m => m.prototype));
return <any>X;
}The first argument is considered to be a base class, and hence, its ctor is called.
You can tweak this in various ways: removing Object.assign (I use it for convenience), call all ctors in chain including your mixins (so that your mixins may initialize state), make the function work without a base class at all, etc.
Here's an updated playground that demonstrate this working with a non-trivial base class:
Playground
function Mixin<T>(...mixins) : new() => T {
class X {
constructor() {
mixins[0].call(this);
}
}
Object.assign(X.prototype, ...mixins.map(m => m.prototype));
return <any>X;
}
abstract class Dog {
bark() { console.log('bark!'); }
}
abstract class Duck {
quack() { console.log('quack!'); }
}
class Base {
constructor() {
console.log('base ctor');
}
world = "world";
hello() {
console.log(`hello ${this.world}`);
}
}
interface DoggyDuck extends Base, Dog, Duck { }
class Mutant extends Mixin<DoggyDuck>(Base, Dog, Duck) {
}
let xavier = new Mutant();
xavier.bark();
xavier.quack();
xavier.hello();In the interests of not reading this thread in detail if possible -- does the code above solve your use case enough that we can close this as having a sufficient solution in place?
It is ok for me, even if i would prefer a syntactic sugar in typescript above this solution.
Best regards
Envoyé de mon iPhone
Le 21 janv. 2016 à 03:03, Ryan Cavanaugh notifications@github.com a écrit :
In the interests of not reading this thread in detail if possible -- does the code above solve your use case enough that we can close this as having a sufficient solution in place?
—
Reply to this email directly or view it on GitHub.
Sorry, I missed this... I have used the pattern mentioned in this issue quite extensively and there is one challenge to it (which I don't think the original proposal addressed either, given the following:
class A {
foo: string = '';
}
class B {
foo() { return 'foo'; };
}
interface GenericClass<T> {
new (...args: any[]): T;
}
function mixin<T, U>(target: GenericClass<T>, source: GenericClass<U>): GenericClass<T&U> {
return;
}
const C = mixin(A, B);
const c = new C();
c.foo; // is of type string & (() => string)You run into issues when you have members that conflict.
As far as the original proposal, I would prefer the #3870 as I think that type of operations that we are talking about could be handled better that way something like:
function mixin<T, ...U>(target: (new () => T), ...mixins: (new () => U)[]): (new () => T & ...U) {
// do mixin
}There are some other considerations too about that discussed in #3870... should ...U be A&B&C... or should it be A|B|C..., etc...
A&B&C... makes more since then A|B|C...
Why don't you just use a mixin decorator?
function mixin(...ifaces) {
...
}
@mixin(Dog, Cat, Mouse)
class MouseyCatDog {
}
This would not require any change to the language at all.
@silkentrance run-time decorators don't modify the type information. class MouseyCatDog would be a class without any properties at design time, though it could have the proper run-time structure, but not having the right design time structure is essentially useless (and dangerous).
@kitsonk Basically, what you are asking for is not a mixin concept but, simply put, a multiple inheritance concept where the first class inherited from would be the actual super prototype and any other classes following that would be treated as mixins, e.g.
class Foo extends Bar, Car, Tar
{}
The generated/augmented constructor would then call upon first Bar.apply(), then Car and last but not least Tar.
If this be true, then instanceof Car and instanceof Tar should not fail. Again, here the transpiler could replace uses of instanceof by a custom implementation that will return true for all mixins and theirs and the super class' prototype chain.
As such, and during runtime, the generated class Foo would need some extra annotations, e.g.
Foo[Symbol['inheritanceChain')] = [Bar, Car, Tar];
Otherwise we talk about IDEs here and their use of the AST produced by the typescript ast generator.
And, according to your last comment on #2900, the use of extraneous files.
Closing as mixin classes are now supported with #13743.