1
0
Fork 0

Began implementing composition-based 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.
perfodd
Mike Gerwitz 2014-01-23 00:34:15 -05:00
parent dfc83032d7
commit 71358eab59
4 changed files with 151 additions and 63 deletions

View File

@ -659,6 +659,12 @@ exports.prototype.createConcreteCtor = function( cname, members )
// generate and store unique instance id // generate and store unique instance id
attachInstanceId( this, ++_self._instanceId ); attachInstanceId( this, ++_self._instanceId );
// handle internal trait initialization logic, if provided
if ( typeof this.___$$tctor$$ === 'function' )
{
this.___$$tctor$$.call( this );
}
// call the constructor, if one was provided // call the constructor, if one was provided
if ( typeof this.__construct === 'function' ) if ( typeof this.__construct === 'function' )
{ {
@ -666,9 +672,10 @@ exports.prototype.createConcreteCtor = function( cname, members )
// subtypes), and since we're using apply with 'this', the // subtypes), and since we're using apply with 'this', the
// constructor will be applied to subtypes without a problem // constructor will be applied to subtypes without a problem
this.__construct.apply( this, ( args || arguments ) ); this.__construct.apply( this, ( args || arguments ) );
args = null;
} }
args = null;
// attach any instance properties/methods (done after // attach any instance properties/methods (done after
// constructor to ensure they are not overridden) // constructor to ensure they are not overridden)
attachInstanceOf( this ); attachInstanceOf( this );

View File

@ -19,6 +19,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var AbstractClass = require( __dirname + '/class_abstract' );
function Trait() function Trait()
{ {
@ -33,13 +35,34 @@ function Trait()
Trait.extend = function( dfn ) Trait.extend = function( dfn )
{ {
// we need at least one abstract member in order to declare a class as
// abstract (in this case, our trait class), so let's create a dummy one
// just in case DFN does not contain any abstract members itself
dfn[ 'abstract protected __$$trait$$' ] = [];
function TraitType() function TraitType()
{ {
throw Error( "Cannot instantiate trait" ); throw Error( "Cannot instantiate trait" );
}; };
// and here we can see that traits are quite literally abstract classes
var tclass = AbstractClass( dfn );
TraitType.__trait = true; TraitType.__trait = true;
TraitType.__dfn = dfn; TraitType.__acls = tclass;
TraitType.__ccls = createConcrete( tclass );
// traits are not permitted to define constructors
if ( tclass.___$$methods$$['public'].__construct !== undefined )
{
throw Error( "Traits may not define __construct" );
}
// invoked to trigger mixin
TraitType.__mixin = function( dfn )
{
mixin( TraitType, dfn );
};
return TraitType; return TraitType;
}; };
@ -51,42 +74,135 @@ Trait.isTrait = function( trait )
}; };
/**
* Create a concrete class from the abstract trait class
*
* This class is the one that will be instantiated by classes that mix in
* the trait.
*
* @param {AbstractClass} acls abstract trait class
*
* @return {Class} concrete trait class for instantiation
*/
function createConcrete( acls )
{
// start by providing a concrete implementation for our dummy method
var dfn = {
'protected __$$trait$$': function() {},
};
// TODO: everything else
return acls.extend( dfn );
}
/** /**
* Mix trait into the given definition * Mix trait into the given definition
* *
* The original object DFN is modified; it is not cloned. * The original object DFN is modified; it is not cloned.
* *
* TODO: we could benefit from processing the keywords now (since we need
* the name anyway) and not re-processing them later for the class.
*
* @param {Trait} trait trait to mix in * @param {Trait} trait trait to mix in
* @param {Object} dfn definition object to merge into * @param {Object} dfn definition object to merge into
* *
* @return {Object} dfn * @return {Object} dfn
*/ */
Trait.mixin = function( trait, dfn ) function mixin( trait, dfn )
{ {
var tdfn = trait.__dfn || {}; // the abstract class hidden within the trait
for ( var f in tdfn ) var acls = trait.__acls,
methods = acls.___$$methods$$,
pub = methods['public'];
// retrieve the private member name that will contain this trait object
var iname = addTraitInst( trait.__ccls, dfn );
for ( var f in pub )
{ {
// this is a simple check that will match only when all keywords, if ( !( Object.hasOwnProperty.call( pub, f ) ) )
// etc are the same; we expect that---at least for the time
// being---class validations will ensures that redefinitions do not
// occur when the field strings vary
if ( dfn[ f ] )
{ {
// TODO: conflcit resolution continue;
throw Error( "Trait field `" + f + "' conflits" );
}
else if ( f.match( /\b__construct\b/ ) )
{
throw Error( "Traits may not define __construct" );
} }
dfn[ f ] = tdfn[ f ]; // TODO: this is a kluge; we'll use proper reflection eventually,
// but for now, this is how we determine if this is an actual public
// method vs. something that just happens to be on the public
// visibility object
if ( !( pub[ f ].___$$keywords$$ ) )
{
continue;
}
// proxy this method to what will be the encapsulated trait object
dfn[ 'public proxy ' + f ] = iname;
} }
return dfn; return dfn;
}
/**
* Add concrete trait class to a class instantion list
*
* This list---which will be created if it does not already exist---will be
* used upon instantiation of the class consuming DFN to instantiate the
* concrete trait classes.
*
* Here, `tc' and `to' are understood to be, respectively, ``trait class''
* and ``trait object''.
*
* @param {Class} C concrete trait class
* @param {Object} dfn definition object of class being mixed into
*
* @return {string} private member into which C instance shall be stored
*/
function addTraitInst( C, dfn )
{
var tc = ( dfn.___$$tc$$ = ( dfn.___$$tc$$ || [] ) ),
iname = '___$to$' + tc.length;
// the trait object array will contain two values: the destination field
// and the class to instantiate
tc.push( [ iname, C ] );
// we must also add the private field to the definition object to
// support the object assignment indicated by TC
dfn[ 'private ' + iname ] = null;
// create internal trait ctor if not available
if ( dfn.___$$tctor$$ === undefined )
{
dfn.___$$tctor$$ = tctor;
}
return iname;
}
/**
* Trait initialization constructor
*
* May be used to initialize all traits mixed into the class that invokes
* this function. All concrete trait classes are instantiated and their
* resulting objects assigned to their rsepective pre-determined field
* names.
*
* @return {undefined}
*/
function tctor()
{
// instantiate all traits and assign the object to their
// respective fields
var tc = this.___$$tc$$;
for ( var t in tc )
{
var f = tc[ t ][ 0 ],
C = tc[ t ][ 1 ];
// TODO: pass protected visibility object once we create
// trait class ctors
this[ f ] = C();
}
}; };

View File

@ -28,8 +28,6 @@ var util = require( __dirname + '/util' ),
MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ), MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ),
wrappers = require( __dirname + '/MethodWrappers' ).standard, wrappers = require( __dirname + '/MethodWrappers' ).standard,
Trait = require( __dirname + '/Trait' ),
class_builder = ClassBuilder( class_builder = ClassBuilder(
require( __dirname + '/MemberBuilder' )( require( __dirname + '/MemberBuilder' )(
MethodWrapperFactory( wrappers.wrapNew ), MethodWrapperFactory( wrappers.wrapNew ),
@ -383,7 +381,7 @@ function createUse( base, traits )
// "mix" each trait into the provided definition object // "mix" each trait into the provided definition object
for ( var i = 0, n = traits.length; i < n; i++ ) for ( var i = 0, n = traits.length; i < n; i++ )
{ {
Trait.mixin( traits[ i ], dfn ); traits[ i ].__mixin( dfn );
} }
return extend.call( null, base, dfn ); return extend.call( null, base, dfn );

View File

@ -60,7 +60,7 @@ require( 'common' ).testCase(
], ],
[ 'bar', [ 'bar',
{ 'virtual bar': function() {} }, { 'virtual bar': function() {} },
{ 'override bar': function() {} }, { 'public bar': function() {} },
], ],
]; ];
}, },
@ -200,9 +200,7 @@ require( 'common' ).testCase(
{ {
try try
{ {
this.Class this.Sut( { __construct: function() {} } );
.use( this.Sut( { __construct: function() {} } ) )
.extend( {} );
} }
catch ( e ) catch ( e )
{ {
@ -222,9 +220,12 @@ require( 'common' ).testCase(
* *
* TODO: conflict resolution through aliasing * TODO: conflict resolution through aliasing
*/ */
'@each(fconflict) Cannot use multiple traits definining same field': '@each(fconflict) Cannot mix in multiple concrete methods of same name':
function( dfns ) function( dfns )
{ {
// TODO: not yet working with composition approach
this.skip();
var fname = dfns[ 0 ]; var fname = dfns[ 0 ];
// both traits define `foo' // both traits define `foo'
@ -256,41 +257,7 @@ require( 'common' ).testCase(
}, },
/** 'Private class members are not accessible to used traits': function()
* Once a trait is mixed in, its methods should execute with `this'
* bound to the instance of the class that it was mixed into, not the
* trait itself. In particular, this means that the trait can access
* members of the class in which it mixes into (but see tests that
* follow).
*/
'Trait methods execute within context of the containing class':
function()
{
var expected = 'bar';
var T = this.Sut(
{
// attempts to invoke protected method of containing class
'public setFoo': function( val ) { this.doSet( val ); },
} );
var C = this.Class.use( T ).extend(
{
'private _foo': null,
'protected doSet': function( val ) { this._foo = val; },
'public getFoo': function() { return this._foo; },
} );
// we do not use method chaining for this test just to ensure that
// any hiccups with returning `this' from setFoo will not compromise
// the assertion
var inst = C();
inst.setFoo( expected );
this.assertEqual( inst.getFoo(), expected );
},
'Private class members are not accessible to useed traits': function()
{ {
// TODO: this is not yet the case // TODO: this is not yet the case
}, },