Traits can now override methods of their class supertypes. Previously, in
order to override a method of some class `C` by mixing in some trait `T`,
both had to implement a common interface. This had two notable downsides:
1. A trait that was only compatible with details of `C` could only work
with `C#M` if it implemented an interface `I` that declared `I#M`.
This required the author of `C` to create interfaces where they would
otherwise not be necessary.
2. As a corollary of #1---since methods of interfaces must be public, it
was not possible for `T` to override any protected method of `C`; this
meant that `C` would have to declare such methods public, which may
break encapsulation or expose unnecessary concerns to the outside
world.
Until documentation is available---hopefully in the near future---the test
cases provide detailed documentation of the behavior. Stackable traits work
as you would expect:
```javascript
var C = Class(
{
'virtual foo': function()
{
return 'C';
},
} );
var T1 = Trait.extend( C,
{
'virtual abstract override foo': function()
{
return 'T1' + this.__super();
},
} );
var T2 = Trait.extend( C,
{
'virtual abstract override foo': function()
{
return 'T2' + this.__super();
},
} );
C.use( T1 )
.use( T1 )
.use( T2 )
.use( T2 )
.foo();
// result: "T2T2T1T1C"
```
If the `override` keyword is used without `abstract`, then the super method
is statically bound to the supertype, rather than being resolved at runtime:
```javascript
var C = Class(
{
'virtual foo': function()
{
return 'C';
},
} );
var T1 = Trait.extend( C,
{
'virtual abstract override foo': function()
{
return 'T1' + this.__super();
},
} );
var T2 = Trait.extend( C,
{
// static override
'virtual override foo': function()
{
return 'T2' + this.__super();
},
} );
C.use( T1 )
.use( T1 )
.use( T2 )
.use( T2 )
.foo();
// result: "T2C"
```
This latter form should be discouraged in most circumstances (as it prevents
stackable traits), but the behavior is consistent with the rest of the
system.
Happy hacking.
This allows ease.js classes to mimic the structure of ES6 classes, which use
`constructor` to denote the constructor. This patch simply aliases it to
`__construct`, which ease.js handles as it would normally.
To that note, since the ES6 `class` keyword is purely syntatic sugar around
the prototype model, there is not much benefit to using it over ease.js if
benefits of ease.js are still desired, since the member definition syntax is
a feature of object literals:
```
// ease.js using ES6
let Person = Class(
{
_name: '',
// note that __construct still works as well
constructor( name ) {
this._name = ''+name;
},
sayHi() {
return "Hi, I'm " + this.getName();
},
// keywords still work as expected
'protected getName'() {
return this._name;
}
} );
// ES6 using `class` keyword
class Person
{
// note that ES6 will _not_ make this private
_name: '',
constructor( name ) {
this._name = ''+name;
},
sayHi() {
return "Hi, I'm " + this.getName();
}
// keywords unsupported (you'd have to use Symbols)
getName() {
return this._name;
}
}
// ES3/5 ease.js
var Person = Class(
{
_name: '',
__construct: function( name ) {
this._name = ''+name;
},
sayHi: function() {
return "Hi, I'm " + this._name;
},
'protected getName': function() {
return this._name;
}
} );
```
As you can see, the only change between writing ES6-style method definitions
is the syntax; all keywords and other features continue to work as expected.
The previous implementation could not survive recursive applications, which
I did know about. But that was before the system was brutally abused by
traits.
A solution for this problem took a disproportionally large amount of time,
attempting many different approaches, and arriving still at a kluge; this is
indicative of a larger issue---we've long since breached the comfort of the
original design, and drastic refactoring is needed.
I have ideas for this, and have already started in another branch, but I
cannot but this implementation off any longer while waiting for it.
Sorry for anyone waiting on the next release: this is what held it up, in
combination with my attention being directed elsewhere during that time (see
the sparse commit timestamps). Including this ordering guarantee is very
important for a stable, well-designed [trait] system.
See test cases for rationale.
I'm not satisfied with this implementation, but the current state of ease.js
makes this kind of thing awkward. Will revisit at some point.
This is an important feature to permit trait reuse without excessive
subtyping---composition over inheritance. For example, consider that you
have a `HttpPlainAuth` trait that adds authentication support to some
transport layer. Without parameterized traits, you have two options:
1. Expose setters for credentials
2. Trait closure
3. Extend the trait (not yet supported)
The first option seems like a simple solution:
```javascript
Transport.use( HttpPlainAuth )()
.setUser( 'username', 'password' )
.send( ... );
```
But we are now in the unfortunate situation that our initialization
procedure has changed. This, for one, means that authentication logic must
be added to anything that instantiates classes that mix in `HttpPlainAuth`.
We'll explore that in more detail momentarily.
More concerning with this first method is that, not only have we prohibited
immutability, but we have also made our code reliant on *invocation order*;
`setUser` must be called before `send`. What if we have other traits mixed
in that have similar conventions? Normally, this is the type of problem that
would be solved with a builder, but would we want every configurable trait
to return a new `Transport` instance? All that on top of littering the
API---what a mess!
The second option is to store configuration data outside of the Trait acting
as a closure:
```javascript
var _user, _pass;
function setCredentials( user, pass ) { _user = user; _pass = pass; }
Trait( 'HttpPlainAuth', { /* use _user and _pass */ } )
```
There are a number of problems with this; the most apparent is that, in this
case, the variables `_user` and `_pass` act in place of static fields---all
instances will share that data, and if the data is modified, it will affect
all instances; you are therefore relying on external state, and mutability
is forced upon you. You are also left with an awkward `setCredentials` call
that is disjoint from `HttpPlainAuth`.
The other notable issue arises if you did want to support instance-specific
credentials. You would have to use ease.js' internal identifiers (which is
undocumented and subject to change in future versions), and would likely
accumulate garbage data as mixin instances are deallocated, since ECMAScript
does not have destructor support.
To recover from memory leaks, you could instead create a trait generator:
```javascript
function createHttpPlainAuth( user, pass )
{
return Trait( { /* ... */ } );
}
```
This uses the same closure concept, but generates new traits at runtime.
This has various implications depending on your engine, and may thwart
future ease.js optimization attempts.
The third (which will be supported in the near future) is prohibitive: we'll
add many unnecessary traits that are a nightmare to develop and maintain.
Parameterized traits are similar in spirit to option three, but without
creating new traits each call: traits now support being passed configuration
data at the time of mixin that will be passed to every new instance:
```javascript
Transport.use( HttpPlainAuth( user, pass ) )()
.send( ... );
```
Notice now how the authentication configuration is isolated to the actual
mixin, *prior to* instantiation; the caller performing instantiation need
not be aware of this mixin, and so the construction logic can remain wholly
generic for all `Transport` types.
It further allows for a convenient means of providing useful, reusable
exports:
```javascript
module.exports = {
ServerFooAuth: HttpPlainAuth( userfoo, passfoo ),
ServerBarAuth: HttpPlainAuth( userbar, passbar ),
ServerFooTransport: Transport.use( module.exports.ServerFooAuth ),
// ...
};
var module = require( 'foo' );
// dynamic auth
Transport.use( foo.ServerFooAuth )().send( ... );
// or predefined classes
module.ServerFooTransport().send( ... );
```
Note that, in all of the above cases, the initialization logic is
unchanged---the caller does not need to be aware of any authentication
mechanism, nor should the caller care of its existence.
So how do you create parameterized traits? You need only define a `__mixin`
method:
Trait( 'HttpPlainAuth', { __mixin: function( user, pass ) { ... } } );
The method `__mixin` will be invoked upon instantiation of the class into
which a particular configuration of `HttpPlainAuth` is mixed into; it was
named differently from `__construct` to make clear that (a) traits cannot be
instantiated and (b) the constructor cannot be overridden by traits.
A configured parameterized trait is said to be an *argument trait*; each
argument trait's configuration is discrete, as was demonstrated by
`ServerFooAuth` and `ServerBarAuth` above. Once a parameterized trait is
configured, its arguments are stored within the argument trait and those
arguments are passed, by reference, to `__mixin`. Since any mixed in trait
can have its own `__mixin` method, this permits traits to have their own
initialization logic without the need for awkward overrides or explicit
method calls.
This is a security-related issue: we want to ensure that the user cannot,
without debugging tools, retrieve certain internal details that may be used
to compromise an implementation.
This will, at least in ES5 environments, prevent its general discovery and
[accidental] use. Of course, any decent debugger will still be able to see
it, so unless ES6 is used, this is not truely hidden.
I do not wholly recall why this was done initially (nor do I care to
research it, since it's not necessary now), but from the looks of it, it was
likely a kluge to handle a poor implementation of some feature.
This will help clean up a little, since it's rude to pollute a prototype
unnecessarily.
Temporary method removed and symbol now provided directly to the trait ctor;
this is needed to retrieve the protected visibility object; there is
currently no API for doing so, and there may never be (providing an API for
such a thing seems questionable).
Of course, if we eventually want to decouple the trait implementation from
ClassBuilder (which would be a good idea), we'll have to figure out
something.
Note that this patch temporarily breaks encapsulation via
ClassBuilder.___$$privsym$$ to expose necessary internal details to Trait;
this will be resolved.
During its initial development, no environments (e.g. Node.js, Chromium,
Firefox) supported strict mode; this has since changed, and node has a
--use-strict option, which is used in the test runner to ensure conformance.
This was a nasty bug that I discovered when working on a project at work
probably over a year ago. I had worked around it, but ease.js was largely
stalled at the time; with it revitalized by GNU, it's finally getting fixed.
See test case comments for more information.
This allows separation of concerns and makes the type system extensible. If
the type does not implement the necessary API, it falls back to using
instanceof.
This reverts commit 58ee52ad4a.
As it turns out, IE9 allows use of instanceof with special object types, not
just functions. One such example is HTMLElement; its type is `object', whereas
in Firefox and Chromium it is `function'. There is no reliable way to perform
this detection.
Therefore, the try/catch here is more reliable.
I feel like I originally did this because older versions of node didn't like
relative paths (unless maybe the cwd wasn't in NODE_PATH). Regardless, it works
now, and this is cleaner.
Further, I noticed that __dirname didn't seem to be working properly with
browserify. While GNU ease.js does not make use of it (ease.js uses its own
scripts), other projects may.
The parser methods are now split into their own functions. This has a number
of benefits: The most immediate is the commit that will follow. The second
benefit is that the function is no longer a closure---all context
information is passed into it, and so it can be optimized by the JavaScript
engine accordingly.
This is a consequence of ease.js' careful trait implementation that ensures
that any mixed in trait retains its API in the same manner that interfaces
and supertypes do.
There does not seem to be tests for any of the metadata at present; they are
implicitly tested through various implementations that make use of them.
This will also be the case here ("will"---in coming commits), but needs to
change.
The upcoming reflection implementation would be an excellent time to do so.
This will allow for additional processing before actually triggering the
warnings. For the sake of this commit, though, we just keep with existing
functionality.
This adds the `weak' keyword and permits abstract method definitions to
appear in the same definition object as the concrete implementation. This
should never be used with hand-written code---it is intended for code
generators (e.g. traits) that do not know if a concrete implementation will
be provided, and would waste cycles duplicating the property parsing that
ease.js will already be doing. It also allows for more concise code
generator code.
Note that, even though it's permitted, the validator still needs to be
modified to permit useful cases. In particular, I need weak abstract and
strong concrete methods for use in traits.
As described in <https://savannah.gnu.org/task/index.php#comment3>.
The benefit of this approach over definition object merging is primarily
simplicitly---we're re-using much of the existing system. We may provide
more tight integration eventually for performance reasons (this is a
proof-of-concept), but this is an interesting start.
This also allows us to study and reason about traits by building off of
existing knowledge of composition; the documentation will make mention of
this to explain design considerations and issues of tight coupling
introduced by mixing in of traits.
On Sun, Dec 22, 2013 at 03:31:08AM -0500, Richard Stallman wrote:
> I hereby dub ease.js a GNU package, and you its maintainer.
>
> Please don't forget to mention prominently in the README file and
> other suitable documentation places that it is a GNU program.
This project was originally LGPLv+-licensed to encourage its use in a community
that is largely copyleft-phobic. After further reflection, that was a mistake,
as adoption is not the important factor here---software freedom is.
When submitting ease.js to the GNU project, it was asked if I would be willing
to relicense it under the GPLv3+; I agreed happily, because there is no reason
why we should provide proprietary software any sort of edge. Indeed, proprietary
JavaScript is a huge problem since it is automatically downloaded on the user's
PC generally without them even knowing, and is a current focus for the FSF. As
such, to remain firm in our stance against proprietary JavaScript, relicensing
made the most sense for GNU.
This is likely to upset current users of ease.js. I am not sure of their
number---I have only seen download counts periodically on npmjs.org---but I know
there are at least a small number. These users are free to continue using the
previous LGPL'd releases, but with the understanding that there will be no
further maintenance (not even bug fixes). If possible, users should use the
GPL-licensed versions and release their software as free software.
Here comes GNU ease.js.
Getters/setters did not get much attention during the initial development of
ease.js, simply because there was such a strong focus on pre-ES5
compatibility---ease.js was created for a project that strongly required it.
Given that, getters/setters were not used, since those are ES5 features. As
such, I find that two things have happened:
1. There was little incentive to provide a proper implementation; even though
I noticed the issues during the initial development, they were left
unresolved and were then forgotten about as the project lay dormant for a
while.
2. The project was dormant because it was working as intended (sure, there
are still things on the TODO-list feature-wise). Since getters/setters were
unused in the project for which ease.js was created, the bug was never
found and so never addressed.
That said, I now am using getters/setters in a project with ease.js and noticed
a very odd bug that could not be explained by that project's implementation.
Sure enough, it was an ease.js issue and this commit resolves it.
Now, there is more to be said about this commit. Mainly, it should be noted that
MemberBuilder.buildGetterSetter, when compared with its method counterpart
(buildMethod) is incomplete---it does not properly address overrides, the
abstract keyword, proxies or the possibility of method hiding. This is certainly
something that I will get to, but I want to get this fix out as soon as I can.
Since overriding ES5 getters/setters (rather than explicit methods) is more
likely to be a rarity, and since a partial fix is better than no fix, this will
likely be tagged immediately and a further fix will follow in the (hopefully
near) future.
(This is an interesting example of how glaring bugs manage to slip through the
cracks, even when the developer is initially aware of them.)