Repository navigation
Mixin language support (with toy implementation) #2919
Description
Activity
- addedDiscussionIssues which may not have code impactIssues which may not have code impact
on Apr 27, 2015 DanielRosenwasser commented
on Apr 27, 2015 MemberMore actionsHey Daniel (@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,
traitvsmixes(one is a noun, the other a verb). Their proposal is based on a new structure type. My version extends the class declaration. I think my proposal is somewhat more lightweight (and, coming from C++,traitsrefer to trait classes which are something else) but they do have a lot more background (both academic and with a variety of use cases I didn't even imagine). The comparison is interesting!Yea I agree with the lightweight point. I believe that is the strength and
weakness of mixins over traits. I'll hedge my bets that only one would get
implemented if that.It's defiantly an interesting topic and in either case I hope that one gets
implemented.Regards,
James
On 27 Apr 2015 7:19 pm, "dbarbeau" notifications@github.com wrote: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, trait vs mixes (one is a noun, the other a verb).
Their proposal is based on a new structure type. My version extends the
class declaration. I think my proposal is somewhat more lightweight (and,
coming from C++, traits refer to trait classes which are something else)
but they do have a lot more background (both academic and with a variety of
use cases I didn't even imagine). The comparison is interesting!—
Reply to this email directly or view it on GitHub
#2919 (comment)
.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,
DanielPS: I finally got emacs-tss to behave! As easy as a symlink in the end :)
Thanks Dan Quirk (@danquirk) . There is a lot more to mixins than I originally thought!
For example, this proposal doesn't satisfy Ryan Cavanaugh (@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
|operator is confusing to me because it reuses the union type syntax in a slightly different context. But I does resemble the bitwise|operator in some way. It supports attribute renaming. I think the decorator approach could be great if there was a syntax to also put it close the mixture declaration. In new code, one would put the mixins "inline" rather than after the fact@mymix // a-la Python, more in-context class Mixture { }
Where
mymiximplements whatever mixing logic is needed. But this is already different from 729 in thatmymixwould be a high-order function taking an original type and returning a new type (which can shadow the original), not an instance (and from there, the|operator would become useless for mixins). Going down that road, there are no mixin classes but high-order decorators (which can mix classes if the user wants to). How all this is implemented and translates to JS is a mystery, specially in the init of the new class.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
this(right after anysupercall).this(the mixture) has all the mixed in attributes available in its own constructor and can give values to them.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?
Daniel (@dbarbeau) Nice hacking
You might find this interesting:
https://wiki.php.net/rfc/traitsThe Scala example at the bottom was also suggested (with mixin), some interesting history there.
Thanks Jon (@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)
Reacted by Ian Bytchek and Sebastian MüllerCan we replace the verb "mixes" with the word "with" (a la Dart):
class A extends B with M1, M2 { }Reacted by Ian Bytchek and Daniel HeyneI agree that the
withseems a little clearer and more descriptive of the intent. Also, it's a little easier to reserve, sincewithisn't a valid identifier already (and won't need new highlighting for a lot of IDEs).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.
"use strict"; /** * Mixins for multiple inheritance. * Usage: * class Derived extends BaseOne implements BaseTwo, BaseThree{ * baseTwoFunction: (a:number)=>any; * baseThreeProperty: string; * * // Apply any missing base class functions to the derived prototype * private static _mixins = Mixins.Apply(Derived, [BaseTwo, BaseThree]); * } */ module Mixins { export function Apply(derivedClass: any, baseClasses: any[]) { var ret: { baseClass: any, functions: { name: string; done: boolean; type: string; }[] }[]; ret = []; baseClasses.forEach(baseClass=> { ret.push({ baseClass: baseClass, functions: [] }); Object.getOwnPropertyNames(baseClass.prototype).forEach(name=> { // ONLY if doesn't already exist. // Is this a property descriptor on the base class prototype? var basePD = Object.getOwnPropertyDescriptor(baseClass.prototype, name); var derivedPD = Object.getOwnPropertyDescriptor(derivedClass.prototype, name); if (basePD && !derivedPD) { Object.defineProperty(derivedClass.prototype, name, basePD); ret[ret.length - 1].functions.push({ name: name, done: true, type: (basePD.get && basePD.set ? "get set" : basePD.get ? "get" : basePD.set ? "set" : basePD.value ? "method" : "") }); } else { ret[ret.length - 1].functions.push({ name: name, done: false, type: (basePD.get && basePD.set ? "get set" : basePD.get ? "get" : basePD.set ? "set" : basePD.value ? "method" : "") }); } }); }); return ret; } }Further thoughts:
It would be good for mixin-aware mixins to be able to be called in the
super()call. I.e. they could provide a methodmixinorinitor something, to be called asBaseTwo.prototype.init.call(this). These would be called in right-to-left order.Note also that currently mixins cannot have protected or private members. See #3854 for a proposal on how to fix this.
22 remaining items
jods (@jods4)
The example I gave was to illustrate a roadblock I ran into (actually earlier today 😛) with the approach you outlined earlier, which didn't have a straightforward way of representing self-types. Naively writingabstract class C extends Awon't work because it'll mix inAagain.You're correct in your analysis that this isn't an issue with self-types, and that a correct ordering of
A, B, Cwill result. The MDN approach supports this.@jiaweihli , How do you implement the MDN's approach in TypeScript? I'm interested to learn. Do you have an example?
Homa Wong (@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();
Daniel Rosenwasser (@DanielRosenwasser) re: my code above ^
It looks like dynamic anonymous classes don't have full type inferencing. If I manually declare: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,
DogreturnsBase & { bark(): void }(more or less). That's why you loose any other type info.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
baseClassas a class anymore.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 😞.
Luckily for me, I only need a single mixin for the problem I'm trying to solve (but I do need to avoid overwrites from repeated inherited types).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 (@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
AB. Also, there is currently no way to model in the types what your actual_extendsdoes. While the particular example you give doesn't have any property conflicts, it quickly falls down when you do and your typing can quite easily not represent the actual code. For example: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
__extendsyou provided cannot be modelled, so it requires the user to know this and provide an final interface that represents it accurately.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.
A & B & ...) somehow. And that requires variadic types first, which don't yet exist.Kitson Kelly (@kitsonk), note that falsandtru (@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:
TypeError: Class constructor XXXXXX cannot be invoked without 'new'Closing as mixin classes are now supported in #13743.
Couldn't TypesScript
extendjust implement the described "workaround" in https://www.typescriptlang.org/docs/handbook/mixins.html so that it does like that when you write: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
super<ancestor>.functionnotation would make sense / be accepted- locked and limited conversation to collaborators
on Jun 18, 2018
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
extendsorimplementskeywords, and thus goes bymixes. The semantic intent of the keyword is that aMixtureis 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.