-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Mixin language support (with toy implementation) #2919
Comments
Hey @dbarbeau, thanks for the heads up on this interesting work! While we're mainly focusing on ES6 work right now, it's interesting to see this what you've done here. Hopefully we'll get some time to sit down, look at some of the changes and, like you said, get an idea of what might be involved if we wanted something like this. |
This looks very similar to the conversation around traits in #311 |
Thanks for the feedback and for pointing out discussion 311. We're going to the same place but they seem to be taking a different way. In both cases there is a new keyword, |
Yea I agree with the lightweight point. I believe that is the strength and It's defiantly an interesting topic and in either case I hope that one gets Regards,
|
The "design decisions" I made were mostly guided by the urge of having something running quickly - which might not be the best way to design a language. That's why, after contemplating Python's MRO, I went against it : I wouldn't get it right in such a short term. I then figured that conceptually I didn't need anything more than what is described in the handbook, just automate it. Mixins are just that, mixins, not inheritance, MRO would be overkill. Thus the "newer shadows older declaration" approach to conflict solving (conflict smashing would better suit). I couldn't decide if users should be warned about the smashing or not. That's also why I chose the "mixes" verb, it'd be easier to grep through TSC how "extends" is implemented if I did something similar. So a bunch of hacks nothing more. The important for me is to have something running. Whatever gets into TS in the end is not important as long as something gets in there to help us mixers! I can then port my code to whatever is chosen. Regards, PS: I finally got emacs-tss to behave! As easy as a symlink in the end :) |
Thanks @danquirk . There is a lot more to mixins than I originally thought! For example, this proposal doesn't satisfy @RyanCavanaugh's "after-the-fact mixing" requirement (371), which I clearly had no clue about, I tend to overlook the interfacing to existing JS code. Issue 311 presents attribute renaming to avoid name conflicts, which allows to do fine mixing instead of brutal shadowing. In the case where the mixins come from one project, chances are that they won't shadow themselves unless it's by design. But when mixing mixins from different origins, attribute renaming can come in handy. Issue 729 seems to more easily satisfy the "after-the-fact mixing", since it is not in the class declaration but as a decorator function. The @mymix // a-la Python, more in-context
class Mixture {
} Where Concerning 1256's Intersection Types (thankfully, the Bool alphabet is rather small) I'm trying to grasp the concept but haven't yet found a resource which doesn't satellize me immediately, so I can't comment right now on how this proposal is related to 1256. Sorry. Daniel |
In this proposal, concerning my issue with calling mixins' constructors, I think I don't need them. The mixins' default constructors are always called with Ah, I noticed that private mixed-in attribute remain inaccessible to the mixture. I don't think it's desirable : the mixture's state IS exactly the mixin's state (state==members), no? |
@dbarbeau Nice hacking You might find this interesting: The Scala example at the bottom was also suggested (with mixin), some interesting history there. |
Thanks @jbondc! That is an interesting read, I hope to get some time to dive deeper into it. Reading all those links leads me to think that, alright my implementation is lightweight and solved an immediate issue for me (with a slight tweak in the mixin precedence : the mixture's implementation should take precedence over mixins' in my very own case, like they do in the PHP link), but really, it is too lightweight to be future proof. What may be emerging is a Type algebra, some sort of meta-programming (there's already type union, talk about type intersection...). If that metaprogramming could be taken to the level of a hypothetic Typescript-Type-DSL (that may just look like ES/JS/TS, and can be parsed like it but works on types, just avoid C++ template-style metaprog :) ), you could write things like this (just letting the thoughts machine run, not meant to be realistic): // Takes type arguments and returns a new type.
// Implements the handbook's mixin algo. Could be part of the stdlib.
function mixin<...Mixins> : <> { // this signature tells the compiler that we're using the type DSL.
// fun with metaprogramming...
// Return new type;
}
// For after-the-fact mixing, maybe?
// Takes type arguments and returns a new type.
// Implements the handbook's mixin algo. Could be part of the stdlib.
function post_mixin<Mixture, ...Mixins> : <> {
// fun with metaprogramming...
// Return new type;
}
// Takes no type arguments and returns a new type
function custom_static_mixin<> : <> {
// The user chose to create a new type from a predefined set of classes.
// Copy properties, rename these, modify flags if you wish because you have a DSL for that.
// Return new type;
}
/*Since mixin returns a type and extends expects a type, this is valid:*/
class MyClass extends mixin<M1,M2,M3> {
}
/*Same here, name ambiguity can be resolved by taking the function which returns a type*/
class MyOtherClass extends custom_static_mixin {
}
/* After the fact? */
class A {
}
A = post_mixin<A, M1, M2, M3>; // mixin and overwrite original type. In this hypothesis functions convey the idea that the user can use a traditional programming metaphor (e.g. imperative) to customize the output type, instead of a declarative way that may need to extend the language syntax and more lexer/parser effort. Daniel |
I do feel that an even better idea would be permitting default interface implementations. Java had this very problem, and fixed it with default interface implementations in Java 8. I feel this could profit from the same addition. (I filed #3536 regarding this alternative) |
Can we replace the verb "mixes" with the word "with" (a la Dart):
|
I agree that the |
Well while we are all at it, here is my take. Anything implemented by a class listed further to the left overrides anything implemented by a class further to the right.
Further thoughts: It would be good for mixin-aware mixins to be able to be called in the Note also that currently mixins cannot have protected or private members. See #3854 for a proposal on how to fix this. |
This and generators are the only real blocking things from me being able to use TypeScript in my project instead of Babel. (I need the generators for laziness, and mixins are used extensively in one of my files, where I would otherwise need a deep multiple-inheritance hierarchy, because of my large number of blurred is-a/has-a relationships in it, most of which need checked at runtime, and so they have to be added as instance properties. |
Is the need for language support met by Intersection Types #3622 (committed)? See also "Feature: type alternatives (to facilitate traits/mixins/type decorators)" #727, and "optional interface" #371 aiming at the same target. Syntax: Synopsis:
Or you could provide a .d.ts which typed the function |
Intersection Types meets the needs of the Typing, but not the emit. |
@RichiCoder1 , the emit is a single line for each class. I'm using mixins all day long using the Mixin module I posted, and I don't think it's a big deal. Does the emit really, really, really need language support? |
The rest operator for Generics is covered under #3870 |
Thanks @kitsonk |
A syntax that closely aligns with the new intersection types would be something like: class Test extends B & C & D {
...
} |
+1 for a with/mixes keyword and constructor restrictions. |
So, really ... multiple inheritance? Would be cool. Plus partials (#563). |
+1 for mixins |
Hello, Well, for what it's worth, I'm trying to rebase my branch on master. Asides merging issues, I'm hitting some errors relative to getApparentType which returns a type where the mixed in members do not exist. The code has evolved quite a bit and I can't wrap my head around the bits involved in the bug. Well, at this point I'm stuck and spent the time I could afford to spend on the rebase. I will retry later unless the discussion finally focuses on some other solution. This hack came in handy for quite a few months but after several refactorings, my work doesn't rely on mixins anymore. I thus have less interest in rebasing too. To be continued. |
There seems to be a recurring pattern here, which is unfair, annoying, and actually counter healthy programming practices. This is exactly the same issue I've complaint about with regards to function overloading. What's a frameworkTypescript is a superset of Javascript. It is a framework in the sense that it takes common behaviour requirements and generalises them. For example, we can now write the Now, if what you have to do to achieve a behaviour is write bespoke code, partly by using code provided code snippets - that's not the definition of something that a framework support. As such, currently typescript has neither support for mixins, nor for function overloading. In the case of the former, we still need to copy the interface, and add some extra code; in the case of the latter, we still need to implement a router per case. While this is fine and understood from prioritisation perspective, it is unfair that the documentation (handbook) claims such support. If by providing examples of how to achieve something that is not generalised by typescript you consider that something to be a feature of typescript, you can just equally claim to support anything imaginable - like animation of social networks. The problemConsider I have two classes in already class hierarchy - To mixin the Now say the interface of True, in the way things are done now, at least no action is needed when the implementation changes. But still, is this really what mixins are about? Absolutely not. What makes matters worse, is that instead of the proposed solution, one is much better just using some pre-compilation method to pull in a source file including both the interface and implementation into the relevant classes. So a change is always needed in one place only. The current solution for mixins promotes code duplication and thus potential inconsistency. And all this, in my view, is in order to falsely claim typescript supports mixins. SuggestionsI honestly think that you should move mixins and function overloading into a section called 'workarounds' or something. You simply can't claim to support these features. No body reads the whole handbook before choosing to opt for typescript. You check if it supports X, Y, Z and make a decision. So it appears by the handbook that typescript supports mixins and function overloading, where in practice it doesn't. |
Those a pretty serious accusations... as if the team are trying to "trick" you into choosing TypeScript. I doubt they set out, to trick everyone into using TypeScript on the back of mixins. The handbook gives an example of how to support that pattern in TypeScript. The current situation is that the TypeScript team are very very cautious about introducing functionality that is likely to conflict with something that TC39 has indicated that they will look at at some point in the future. There have been several things that the TypeScript team would take back, considering where TC39 ended up (e.g. modules). I have personally chatted with a member of TC39 and they indicated that native "mixin" or "composition" type support for classes is that something that was part of the original class proposal, but got too heated and too contended and so it got pulled out, to be revisited at a future date. TC39 has made it abundantly clear this is an area that they will address at some point, though I am personally disappointed that it doesn't seem to be at the forefront of any of the members agendas at this moment. My understanding (based on comments by @RyanCavanaugh) is that the team would consider something if they think it is far enough in the future, that TypeScript has a vested interest in that domain and that the "TypeScript solution" could win as the final syntax in TC39. I don't see that happening with mixins. We (Dojo) like mixins and composition and we like TypeScript. We were able to get something acceptable, with a level of type inference out of TypeScript that addresses some of the concerns you point out about about. While it isn't straight forward and simple, it is possible to get to a point where it is simple for the end developer. |
Perhaps it is worth directing people to #6502, where @jods4 proposes a mixin solution that I suspect some will find better than the current handbook proposal: 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(); |
This doesn't seem to work if |
@jiaweihli I made up that sample code to show how we could make TS typing work. function Mixin<T>(base, ...mixins) : new() => T {
X.prototype = new base();
X.prototype.constructor = X;
function 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!'); }
}
class BaseBase {
constructor() {
console.log('base base ctor');
}
ping() {
console.log('pong');
}
}
class Base extends BaseBase {
constructor() {
super();
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();
xavier.ping(); |
@jods4 For a concrete example of when this is a problem: consider the case where I want to create a new class In the end, I decided to go with MDN's approach, which works seamlessly with TypeScript though it's a little bit more verbose versus having direct language support. I think the MDN approach should be the recommended approach over what's currently shown in the handbook, which was written when things like local classes weren't yet supported. Thoughts @DanielRosenwasser ? |
@jiaweihli I'm not familiar with those concepts so I won't be able to help much here. I found one thing weird, though. You said "C needs A as a prereq", which I understand is what self-types is about and is explicitely different from "C is a A". But then you said that mixing MDN approach looks good btw if it works properly in TS. There's not much verbosity, maybe a little in the combination of the mixins. Consider that the toy implementation above currently requires an interface for each combination of mixins, which is not ideal either. Only drawback I see is that lots of intermediate classes are created if you compose many mixins. Probably not a problem, though. |
@jods4 You're correct in your analysis that this isn't an issue with self-types, and that a correct ordering of |
@jiaweihli , How do you implement the MDN's approach in TypeScript? I'm interested to learn. Do you have an example? |
@unional Here's what I'm using right now: const Dog =
// type prerequisite: can only mixin to classes derived from Base,
// but won't mixin Base again.
(baseClass: { new(...args: Array<any>): Base }) => {
return class extends baseClass {
bark(): void { console.log('bark'); }
};
};
const Duck =
(baseClass: { new(...args: Array<any>): Base }) => {
return class extends baseClass {
quack(): void { console.log('quack'); }
};
};
class BaseBase {
constructor() {
console.log('base base ctor');
}
ping() {
console.log('pong');
}
}
class Base extends BaseBase {
constructor() {
super();
console.log('base ctor');
}
world = "world";
hello() {
console.log(`hello ${this.world}`);
}
}
class Mutant extends Dog(Duck(Base)) {}
let xavier = new Mutant();
xavier.bark();
// :KLUDGE: Type inferencing doesn't work for any mixins after the first.
// But the compiled code is correct.
xavier['quack']();
xavier.hello();
xavier.ping(); |
@DanielRosenwasser re: my code above ^ const Quacker = class extends Base {
quack(): void { console.log('quack'); }
};
const BarkQuacker = class extends Quacker {
bark(): void { console.log('bark'); }
};
(new BarkQuacker).quack(); then things work as expected. Is there any chance of improving the above issue? If this is fixed, then traits can work with no additional effort. |
@jiaweihli The :KLUDGE: is a major drawback, you loose all typing for all but one mixin... The problem is that statically, Dynamic type info is not possible (by definition) but the closest thing is generics. Unfortunately, when I try function Dog<T extends Base>(baseClass: { new(...args: Array<any>): T }) => {
return class extends baseClass {
bark(): void { console.log('bark'); }
};
}; It does not compile because TS does not recognize At its hearth, this is the same limitation that requires the supplementary interface in my code above. |
Yeah, I didn't realize this issue until today when I tried to apply the approach to multiple mixins 😞. The multiple mixins compose correctly from the compiled code, but it seems like some extra work is needed to fix the type inferencing in the compiler. |
Edit: fix misplaced variable Wonder if this could be solved with rest types? interface MixinType1<T extends Object, ...U extends Object[]>
extends T & MixinType<...U> {}
type MixinType<T extends Object, ...U extends Object[]> = T & MixinType1<...U>
function mixin<T extends Object, ...U extends Object[]>(host: MixinType<T, ...U>, ...objects: ...U)
function mixin<
T extends Object & {[key: string]: U},
U,
...V extends Array<Object | {[key: string]: U>
>(host: MixinType<T, ...V>, ...objects: ...V)
function mixin(host: {[key: string]: any}, ...args: {[key: string]: any}[]) {
for (const arg of args) {
for (const key of Object.keys(arg)) {
(<any> host)[key] = (<any> arg)[key]
}
}
return host
} |
My polyfill: export function Mixin<T>(...mixins: Array<new (...args: any[]) => any>): new (...args: any[]) => T {
return mixins.reduceRight((b, d) => __extends(d, b), class { });
}
function __extends(d: new () => any, b: new () => any): new () => any {
const __ = class {
constructor() {
return d.apply(b.apply(this, arguments) || this, arguments);
}
};
void Object.assign(__.prototype, d.prototype, b.prototype);
for (const p in b) if (b.hasOwnProperty(p)) __[p] = b[p];
for (const p in d) if (d.hasOwnProperty(p)) __[p] = d[p];
return __;
} it('2', () => {
let cnt = 0;
class A {
constructor(n: number) {
assert(++cnt === 1);
assert(n === 0);
}
static a = 'A';
ap = 'a';
am() {
return this.ap;
}
}
class B {
constructor(n: number) {
assert(++cnt === 2);
assert(n === 0);
}
static b = 'B';
bp = 'b';
bm() {
return this.bp;
}
}
interface AB extends B, A {
}
class X extends Mixin<AB>(B, A) {
constructor(n: number) {
super(n);
assert(++cnt === 3);
assert(n === 0);
}
static x = 'X';
xp = 'x';
xm() {
return this.xp;
}
}
const x = new X(0);
assert(++cnt === 4);
assert(x instanceof A === false);
assert(x instanceof B === false);
assert(x instanceof X);
assert(X['a'] === 'A');
assert(X['b'] === 'B');
assert(X.x === 'X');
assert(x.ap === 'a');
assert(x.bp === 'b');
assert(x.xp === 'x');
assert(x.am() === 'a');
assert(x.bm() === 'b');
assert(x.xm() === 'x');
}); |
@falsandtru thanks for sharing. Of course a polyfill generally is a term to provide functionality for some standard that exists when not natively supported. I am unaware of any standard that currently exists in ECMAScript (partly why the TypeScript have been loathe to address it). Your solution, like a lot of other solutions, would benefit from the rest operator for generics, but even then there are still some problems with all of this. While not a problem, it is a significant amount of boilerplate to create the transitory interface class A {
a: string;
}
class B {
a(): string {
return 'a'
}
}
interface AB extends B, A { } // Error cannot simultaneously extend
type ABType = B & A; // Can't be used with Mixin, but is type `string & () => string`
class C extends Mixin<AB>(A, B) {
c: number;
} This are similar problems we have run into with dojo-compose although we have been able to get a level of type inference working where in many use cases, the user doesn't have to do any boilerplate, thought our method resolution, like the This is why I hope, at some point, we are provided with a way to programatically modify the types, either through ambient decorators or more feature rich type operators. |
Based on what I'm starting to see in ES developments, I highly suspect that mixins will end up as simple decorators, and if it ever becomes part of the language, it would just end up a new builtin function. You could do it this way in ES + decorators: const wm = new WeakMap()
export function mixin(Cls) {
class C extends Cls {
static [Symbol.hasInstance](obj) {
return Array.from(wm.get(C)).some(C => obj instanceof C)
}
}
Object.defineProperty(C, "name",
Object.getOwnPropertyDescriptor(Cls, "name"))
wm.set(C, new Set())
return C
}
export function mixes(...mixins) {
return Cls => {
for (const m of mixins) {
if (!wm.has(m)) {
throw new TypeError(`Cannot add non-mixin ${m.name} as a mixin`)
}
wm.get(m).add(Cls)
}
for (const proto of mixins.map(m => Object.getPrototypeOf(m.prototype)) {
Object.getOwnPropertyNames(proto)
.filter(prop => prop !== "constructor")
.forEach(prop => {
Object.defineProperty(Cls.prototype, prop,
Object.getOwnPropertyDescriptor(proto, prop))
})
}
}
} If anything, the only way you can realistically get this kind of mixin in TypeScript is by supporting variadic type intersection (i.e. |
@kitsonk, note that @falsandtru 's solution will throw an error when the compilation target is ES6 and up. You can not treat a class as a function in ES6. Invoking a class constructor results in the following error:
|
Closing as mixin classes are now supported in #13743. |
Couldn't TypesScript class DaughterClass extends MotherClass, FatherClass, MailManClass {
// additional implementation + constructor implementation that calls all super constructors
constructor(public value: any) {
super<Motherclass>.constructor(value);
super<FatherClass>.constructor(value);
super<MailManClass>.constructor(value);
doSomethingMore(value);
}
niceCollidingMethod(aParameter: any) {
super<MotherClass>.niceCollidingMethod(aParameter);
aCommandToExecuteInBetween(aParameter);
super<FatherClass>.niceCollidingMethod(aParameter);
return finalStuffToDo(aParameter);
} and then a requirement to re-implement methods or members that collide (diamond problem) which then has to call the super methods in addition to additional actions. Not sure if the suggested |
Hello TypeScripters.
I might be opening a can of worms which might bring an age of darkness upon us (but see PS). Anyway, I've done an attempt to add language support for mixins.
For the user, it is similar to the
extends
orimplements
keywords, and thus goes bymixes
. The semantic intent of the keyword is that aMixture
is NOT a derived class of itsMixins
, it just copies their static and dynamic properties.Several mixins can be supplied, the order determines which property will finally be mixed (newer definitions shadow previous definitions).
What this does is roughly equivalent to http://www.typescriptlang.org/Handbook#mixins, except there is no need to copy attributes manually, so mixins become more manageable - hopefully.
Which emits
and outputs
At this point it seems to work with target ES<6 with quite some limitations (the user can't pass arguments to the mixin constructors). Type checking seems to be working (although I could not make Eclipse or Emacs behave with the new keyword). But be warned Very early experimental preview code from a bad JS programmer who knew nothing about TSC's internals three days ago and who was in a rush. Be warned.
I did this to overcome a maintainability issue with my code and rushed this implementation. It was also an exercise to learn more about TSC. I'm also aware that it might not be a great idea to add keywords to a language each time one gets into trouble, but if others see interest in it or you have suggestions, the branch is here https://github.com/dbarbeau/TypeScript/tree/mixes. Just don't expect this implementation to be stable/complete/anything. Actually, if it can just spark some interest/discussion on how to make mixins more useable, then I'm ok with that.
Daniel
PS: Don't get mad at me :)
PS: I'm now testing this with more "serious code", I will quickly see if it is indeed practical.
The text was updated successfully, but these errors were encountered: