about lightweight traits
On Thu, Feb 12, 2015 at 8:35 AM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:
Without going down full specification/implementation details, does anyone believe that classes should/could be used, in the future, as traits/mixins too?
I do. But I am in no rush. Classes need to grow in several other directions first, and many of these are much higher priority. If classes can no longer be extended to serve as traits, I am fine with dropping this as well.
I find that an anty pattern.
I think traits should be just plain objects with an initializer or some special object flagged as trait and I'd rather leave inheritance and classes features outside this future feature.
Thoughts? Thanks!
See traitsjs.org and previous stale strawmen strawman:syntax_for_efficient_traits, strawman:classes_with_trait_composition, strawman:trait_composition_for_classes, strawman:classes_as_inheritance_sugar
I would agree.
How would you use classes as traits -- are you talking about multiple inheritance? Or flattened to a linear inheritance model?
On Feb 12, 2015, at 8:35 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:
Without going down full specification/implementation details, does anyone believe that classes should/could be used, in the future, as traits/mixins too?
I find that an anty pattern.
I think traits should be just plain objects with an initializer or some special object flagged as trait and I'd rather leave inheritance and classes features outside this future feature.
Thoughts? Thanks!
I would agree with that. I had a trait implementation that used classes as traits, and it caused a lot of problems, especially since classes have constructors. Traits should not have constructors. I have since updated my implementation to use plain objects.
This is what I am using now: gist.github.com/lukescott/36453a75c39c539f5c7d, gist.github.com/lukescott/36453a75c39c539f5c7d
this thousand times ... Traits as class makes no sense to me indeed and Mark example shows plain objects passed as Trait.create ... that works just fine, except some trait initialization, which should not be a constructor, could be used too.
Is there any other language that uses same classical OOP classes as traits too? It really does not feel right to me.
Btw, reason I'm asking is because somebody else asked, I'm really OK to not rush and wait to see how it goes when it's the right time.
When I brought this up my thoughts were to have the classes flattened to a linear inheritance model. Yeah this means an ownProp copy from the constructor for statics and the prototype for instance onto methods. That might be a limitation but currently class syntax is javascript's nicest way to describe behavior units.
On Feb 12, 2015, at 10:27 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:
This is what I am using now: gist.github.com/lukescott/36453a75c39c539f5c7d, gist.github.com/lukescott/36453a75c39c539f5c7d
Based on the gist I provided, this is how I would envision the ES7 version looking like:
trait Trait1 {
method1() {}
}
trait Trait2 {
method2() {}
}
trait Trait3 {
mixin Trait2;
method3() {}
}
Trait3 hasTrait Trait2 // true
Trait3 hasTrait Trait1 // false
class Foo {
mixin Trait1;
}
Foo hasTrait Trait1 // true
Foo hasTrait Trait2 // false
class Foo2 extends Foo {}
Foo2 hasTrait Trait1 // true
Foo2 hasTrait Trait2 // false
class Foo3 extends Foo {
mixin Trait2;
}
Foo3 hasTrait Trait1 // true
Foo3 hasTrait Trait2 // true
class Bar {
mixin Trait3;
}
Bar hasTrait Trait1 // false
Bar hasTrait Trait2 // true
Bar hasTrait Trait3 // true
As far as conflict resolution goes, you could go by trait order (last one wins), or simply throw an exception when mixing in two traits with the same methods. A conflict would look like this:
trait Trait1 {
someMethod()
}
trait Trait2 {
someMethod()
}
class Foo {
mixin Trait1, Trait2;
}
OR
class Foo {
mixin Trait1
mixin Trait2;
}
But this would not be a conflict:
trait Trait1 {
someMethod()
}
trait Trait2 {
mixin Trait1;
someMethod() // Override method from Trait1
}
class Foo {
mixin Trait2;
}
PHP has traits, but has a way to resolve conflicts by specifying which method should be used. I would keep it simple and make conflicts an error outright or last one wins. Most use cases for traits is including functionality that can’t be inherited where you would have to copy and paste methods otherwise.
Traits as class make perfect sense when you consider that classes are functions and so are traits.
So you are saying we can just forget about extends keyword and use mixin instead for everything, right?
Why does ES even need traits? The only aspect they can help with here is the type system we don't have yet.
We have Object.assign
that works fantastically for most classic trait use cases.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Benjamin Gruenbaum
We have
Object.assign
that works fantastically for most classic trait use cases.
Well, actually, it works extremely poorly. The old (now dead or deferred) Object.mixin
, once called Object.define
, is a better fit. But it still fails to account for a couple things, off the top of my head:
- Being able to add custom initialization logic to the class constructor when "mixing in" a trait. You can construct a custom protocol around this, but (a) that means the class you're mixing in to needs to be aware of the protocol; (b) everyone needs to agree on a protocol across the ecosystem.
- Pretty much anything to do with private state doesn't work anymore.
Those points are good, let me try to address them
State - In other languages like C# and swift the issue with adding custom state can be indeed very painful - in JS we can add a symbol on the object mixed into or lazily creating it when the method first executes with a default value. A Symbol can be used for separation of state from the object if that is desired. I don't understand the argument about internal state - how would a mixin be aware of a class's internal not relating to it state anyway?
Protocol - I agree here, but can't you call .assign
in the constructor?
Object.assign
is good only to define defaults properties, since it does
not even understand getters and setters, it's really a poor option for
anything trait related.
Object.assign
is an ES6 introduction based on ES3 assumptions, not even
sure why someone would consider it for traits.
Moreover, initialization on new ClassWithTraits
is something we'll
eventually have to agree about, and here assign
is poor indeed as Domenic
said already.
On top of that, instanceof SomeTrait
makes again no sense to me, 'cause
that's class related and not behavior related, hence my idea that
classes as traits are somehow misleading, confusing, or a mistake.
These two cannot be the same, otherwise we need to agree on which one is redundant ( and I already have an idea which one, now flagship of the incoming standard, would loose "the competition" )
Traits are different than classical inheritance. They simply copy functionality into a class as if the method was actually defined in the class. It isn’t inheritance. It allows you to DRY.
A real world example in my ES6 project:
- Node
- Element extends Node
- Field extends Element
- TextField extends Element
- Select extends Element, mixin Parent
- Option extends Element
- Fieldset extends Element, mixin Parent
- Field extends Element
- Element extends Node
In this case (subset of my class structure) a Fieldset has child Nodes/Elements/Fields. A Field and TextField does not have children. Yet, Select has specific child nodes. I use a trait to mixin the Parent trait to reuse functionality. Select is a parent because it has the trait, but the parent class Field is not a parent. In my case it makes no sense to re-implement that in Select, especially when having the Parent trait means something when building something like a lookup table.
I think it’s important to note that a trait knows very little of the class that implements it. For example I may mixin Parent into Select or Fieldset, but I restrict myself to methods found in Node (my base class) to be able to use the trait in any situation. If I need functionality from another trait, I mixin the other trait to make one trait.
You are also probably forgetting class methods are non enumerable, and assign works only with enumerable and it copies/assigns retrieved values, not descriptors.
Please let's drop Object.assign
from the traits discussion already, since
it won't work with any ES5+ feature, thanks.
On Feb 12, 2015, at 12:11 PM, Domenic Denicola <d at domenic.me> wrote:
- Being able to add custom initialization logic to the class constructor when "mixing in" a trait. You can construct a custom protocol around this, but (a) that means the class you're mixing in to needs to be aware of the protocol; (b) everyone needs to agree on a protocol across the ecosystem.
The biggest issues of trait initializers are (constructors) are:
- What order do you call them in.
- How do you pass in parameters into them.
- They would need to be called by the class constructor. Does this happen at the beginning or end?
I would argue that traits should not have initializers. It’s much easier to do something like this:
trait Trait1 {
initTrait1() {
// this is a regular method
// init trait here
}
}
class Trait1 {
mixin Trait1;
constructor() {
// do work here…
this.initTrait1()
// do more work here...
}
}
This kind of pattern makes initializing traits more intentional and leaves out any assumptions of what the correct order would be. And not all traits need initializers, so it leaves out a lot of complexity.
So we agree traits are not classes and notions, as well as functionality,
is different. Then why would you us class
to define a trait?
How are you going to recognize them?
I have a library that works already out of the box with traits but these are not classes, just objects that will enrich classes.
Overriding methods means having a warning in console, instead of throwing, and nothing else. You compose whatever you want, order of defined mixins/traits matters, as well as eventual initialization of each trait.
The latter one, is available already once each instance is defined, inside the constructor. This works to me, and I'd rather avoid the burden to walk all inheritance chain if I have to accept classes too instead of objects, but I wouldn't mind implementing it as long as I understand why that's a good thing to have in JS.
Why does ES even need traits? The only aspect they can help with here is the type system we don't have yet.
We have
Object.assign
that works fantastically for most classic trait use cases.
They’d provide a way to explain WebIDL concepts like partial interfaces and partial dictionaries. That in itself is pretty cool. If traits were extendable and allowed providing default implementations of methods, it would also allow people to polyfill methods of partial interfaces in a much easier fashion, without having to keep track of all the various types that implement the partial interface.
That seems like a pretty good reason to make them a “thing”, to me.
Luke, answering in order:
-
defining order, first come, first serve ... same as everything else on daily usage (events, Array of callbacks, etc)
-
You do not ever pass parameter to them, parameters are constructors matters, traits should be independent and unaware of constructors arguments otherwise you loose portability. A trait should be able to work and enrich any sort of class, not just some specific one it has no notion about.
-
before everything else, since these should be stand-alone behaviors unrelated with constructors
All marked as IMO, of course :-)
Luke, forgot one detail: you do not explicitly initialize traits, these are
initialized if these have an init
or initialize
method defined, and
before the constructor is executed. So, using you example code:
trait Trait1 {
initt() {
this.trait1Data = {what:'ever'};
}
}
class Trait1 {
mixin Trait1;
constructor() {
console.log(this.trait1Data);
// {what:'ever'}
}
}
That makes them really portable, the class is instantly aware of them since it used them as mixin, but traits are fully independent.
Do you need a lazy trait? Then you can expose an explicit method to set-it-up and invoke it whenever you want in your constructor.
Does any of this make sense? It works already in some case of mine :-)
I know what you’re going for, and I had this in my initial implementation. The issue then becomes:
If Trait2 mixes in Trait1, does the init method get replaced? Traits are about inserting functionality, not about inheritance. Example:
trait Trait1 {
init() {
// init Trait1
}
}
trait Trait2 {
mixin Trait1;
init() {
// init Trait 2
}
}
class Foo {
mixin Trait2;
constructor() {
// do Foo stuff...
}
}
So either init is special and the call order is Trait1.init() Trait2.init() (while other methods get replaced) or Trait2.init() replaces the Trait1.init(). Also since traits are not classes, they cannot call super. You also are defying the call stack because trait initializers “just happen” internally. There is no trail to step through, unlike classes which leaves a trail of breadcrumbs with super() calls.
This version is actually easy to follow:
trait Trait1 {
initTrait1() {
// init Trait1
}
}
trait Trait2 {
mixin Trait1;
initTrait2() {
this.initTrait1();
// init Trait 2
}
}
class Foo {
mixin Trait2;
constructor() {
this.initTrait2();
// do Foo stuff...
}
}
True, each class has to call the custom init methods. But it still addresses 95% of the problem: Adding in common functionality that doesn’t fit in classical inheritance. Also since some traits may expect to be mixed in to a subclass of a certain base, it allows you to call super before the trait initializers, and that’s something you already have to do in your local constructor, so initializing traits in the same way makes sense.
The only thing extra beyond copying implementation into a class or another trait is keeping track of what traits have been mixed in for a “hasTrait” check. This isn't necessarily required as you can easily check for a method you want to call, but it does allow you to make sure that the method belongs to trait and check once for a group of methods.
(This is my ES6 implementation of traits that I’m using now: gist.github.com/lukescott/36453a75c39c539f5c7d)
As for JavaScript, I strongly believe, Role based composition patterns like Mixins or Traits should be function based.
Whereas the widely used purely object based approach (together with some copy mechanics provided by one of the several [extends] implementations) does still work for Mixins, it is going to fail with Traits.
Traits have to provide/implement functionality for conflict resolution of methods, for requiring and for excluding them. For JavaScript this can be achieved easily by just making use of the module pattern and another function based mixin, that does provide those mentioned composition methods.
Function based Mixins/Traits have to be applied by [call] or [apply] only.
I always strongly will argue that a function based approach is most natural for this languages design since it does provide delegation for free and at the same time also enables injecting of and passing around additional state for both Mixins and Traits which is another appealing feature one gets for free too with using [call] or [apply].
And answering the OP's question; Role based composition in JavaScript should work on both levels instance/object level and class level. For this reason alone one should not go for using "Classes" as Mixins.
Maybe future language specifications should only describe the syntactic sugar, that shadows/encapsulates the usage of function based Mixins/ Traits as described e.g. with [1].
[1] The linked article should not be considered spam or selfish - I'm the author of it. Maybe it helps clarifying my point of view, since it is a brief deep dive into the matter of Roles, Mixins, Traits, Talents within the context of JavaScript - [peterseliger.blogspot.de/2014/04/the-many-talents-of-javascript.html#the-many-talents-of-javascript-for-generalizing-role-oriented-programming-approaches-like-traits-and-mixins].
On Feb 12, 2015, at 1:13 PM, Peter Seliger <peter.seliger at googlemail.com> wrote:
As for JavaScript, I strongly believe, Role based composition patterns like Mixins or Traits should be function based.
I assume by this you mean that it shouldn’t be a construct and any special keywords. If that’s the case:
This can be done now already. The problem is in order to do this you have to make a copy of the class you are extending w/ attached methods. And you cannot add traits without already extending an existing class. Assuming of course if you want to keep those definitions at the top instead of the bottom.
Adding a “trait” construct along with a “mixin” or “use” keyword inside class / trait makes them more intuitive to use.
Whereas the widely used purely object based approach (together with some copy mechanics provided by one of the several [extends] implementations) does still work for Mixins, it is going to fail with Traits.
Traits have to provide/implement functionality for conflict resolution of methods, for requiring and for excluding them. For JavaScript this can be achieved easily by just making use of the module pattern and another function based mixin, that does provide those mentioned composition methods.
It doesn’t have to. Conflicts can be a fatal error at definition. There is no need to overcomplicate traits to this extent just because other languages do so. If a conflict does occur it’s likely due to a trait being too large and a sign that the functionality should be decomposed into more traits. A trait differs from a mixin by being able to ask the question “does this class/trait have this trait”. Conflict resolution is a “feature” that needlessly over complicates traits to the point where they become little different from multiple inheritance. — All IMO of course.
It looks like we agree traits are not classes so that classes should not be used or accepted as traits ... which is already an answer to my initial question.
Other "problems"
init is a special case, as constructor is. You don't need a constructor to create an instance, you might not need to initialize a trait in order to be used.
However, trait brings functionality, and this should be consistent. If a class forgets to initialize a trait this should work regardless. If a class does not need a constructor, traits should be usable/initialized automatically regardless.
This is the implicit nature of my proposed init
. To go your way,
initTrait1() as lazy, explicit method you can always do that.
If a trait requires some specific info during initialization, you can
always use this.initTrait1(info)
inside any class method, but that's
something different from the initial, optional, setup such trait might
need, a setup that, if present, will be granted inside the implicit init
What happens inside is also not a class responsibility, class simply has enriched prototype with methods, when that init should occur is not necessarily a class matter while those methods should instead work ASAP. An implicit initializer covers already 100% of cases, it can be absent and go lazy initializer any time.
Last question: would an init override another init? Well, using as little as possible stand-alone logic to compose traits, I haven't personally found a case when Trait2 mixins Trait1 but I believe that's how I'd go
trait Trait2 {
mixin Trait1;
init() {
Trait1.init.call(this);
this.trait2Data = {any:'thing'};
}
}
init is transparent once mixed in, but reachable regardless explicitly.
Same as it is for constructor, nothing new to learn here, we are used to
OtherClass.call(this)
now simplified via super.constructor()
Last bit of info: all this works already in es-class with traits specified as objects, here more examples: WebReflection/es-class/blob/master/FEATURES.md#with
Without going down full specification/implementation details, does anyone believe that classes should/could be used, in the future, as traits/mixins too?
I find that an anty pattern.
I think traits should be just plain objects with an initializer or some special object flagged as trait and I'd rather leave inheritance and classes features outside this future feature.
Thoughts?