1
0
Fork 0

Trait class extend support

master
Mike Gerwitz 2015-05-28 00:00:59 -04:00
commit 7ce57b7d97
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
8 changed files with 557 additions and 59 deletions

View File

@ -12,12 +12,11 @@ complete.
* TODO Trait Extending
Currently, the only way for a trait to override methods of a class
it is being mixed into is to implement a common interface. Traits
should alternatively be able to "extend" classes, which will have
effects similar to Scala in that the trait can only be mixed into
that class. Further, traits should be able to extend and mix in
other traits (though such should be done conservatively).
Traits are able to "extend" classes, thereby declaring interface
compatability; this is a useful alternative to interfaces when a trait is
designed to augment a specific type. This convenience should be extended
to traits: a trait should be able to "extend" another trait in the same
manner that it may extend a class.
* TODO Documentation
Due to the trait implementation taking longer than expected to
@ -78,11 +77,5 @@ complete.
improvement. Until that time, be mindful of the performance test
cases in the =test/perf= directory.
* TODO Intermediate object as class
The immediate syntax---=Foo.use(T)()=---is a short-hand equivalent
of =Foo.use(T).extend({})()=. As such, for consistency, =Class.isA=
should consider the intermediate object returned by a call to =use=
to be a class.
If we are to do so, though, we must make sure that the entire class
API is supported.

View File

@ -144,15 +144,6 @@ function ClassBuilder( warn_handler, member_builder, visibility_factory )
*/
this._instanceId = 0;
/**
* Set to TRUE when class is in the process of being extended to ensure that
* a constructor can be instantiated (to use as the prototype) without
* invoking the class construction logic
*
* @type {boolean}
*/
this._extending = false;
/**
* A flag to let the system know that we are currently attempting to access
* a static property from within a method. This means that the caller should
@ -242,6 +233,30 @@ exports.getMeta = function( cls )
}
/**
* Allow OBJ to assume an identity as a class
*
* This is useful to use objects in situations where classes are expected,
* as it eliminates the need for handling of special cases.
*
* This is intended for internal use---there are no guarantees as to what
* methods ease.js may expect that a class-like object incorporate. That
* guarantee may exist in the future, but until then, stay away.
*
* @param {Object} obj object to masquerade as an ease.js class
*
* @return {Object} OBJ
*/
exports.masquerade = function( obj )
{
// XXX: this is duplicated; abstract
util.defineSecureProp( obj, _priv, {} );
createMeta( obj, exports.ClassBase );
return obj;
};
/**
* Determines if the class is an instance of the given type
*
@ -321,9 +336,6 @@ exports.prototype.build = function extend( _, __ )
{
var build = this;
// ensure we'll be permitted to instantiate abstract classes for the base
this._extending = true;
var a = arguments,
an = a.length,
props = ( ( an > 0 ) ? a[ an - 1 ] : 0 ) || {},
@ -339,18 +351,19 @@ exports.prototype.build = function extend( _, __ )
props: this._memberBuilder.initMembers(),
},
meta = exports.getMeta( base ) || {},
// constructor may be different than base
pmeta = exports.getMeta( prototype.constructor ) || {},
abstract_methods =
util.clone( meta.abstractMethods )
util.clone( pmeta.abstractMethods )
|| { __length: 0 },
virtual_members =
util.clone( meta.virtualMembers )
util.clone( pmeta.virtualMembers )
|| {}
;
// prevent extending final classes
// prevent extending final classes (TODO: abstract this check)
if ( base.___$$final$$ === true )
{
throw Error(
@ -464,7 +477,7 @@ exports.prototype.build = function extend( _, __ )
util.defineSecureProp( prototype, '__self', new_class.___$$svis$$ );
// create internal metadata for the new class
var meta = createMeta( new_class, base );
var meta = createMeta( new_class, base, pmeta );
meta.abstractMethods = abstract_methods;
meta.virtualMembers = virtual_members;
meta.name = cname;
@ -476,15 +489,13 @@ exports.prototype.build = function extend( _, __ )
// (intended for use in prototype chains)
new_class.asPrototype = function()
{
build._extending = true;
var inst = new_class();
build._extending = false;
new_class[ _priv ].extending = true;
var inst = new new_class();
new_class[ _priv ].extending = false;
return inst;
};
// we're done with the extension process
this._extending = false;
return new_class;
};
@ -498,7 +509,9 @@ exports.prototype._getBase = function( base )
// constructor (we could also check to ensure that the return value of
// the constructor is an object, but that is not our concern)
case 'function':
return new base();
return ( base[ _priv ] )
? base.asPrototype()
: new base();
// we can use objects as the prototype directly
case 'object':
@ -841,8 +854,8 @@ exports.prototype.createCtor = function( cname, abstract_methods, members )
*/
exports.prototype.createConcreteCtor = function( cname, members )
{
var args = null,
_self = this;
var args = null,
_self = this;
/**
* Constructor function to be returned
@ -872,7 +885,7 @@ exports.prototype.createConcreteCtor = function( cname, members )
// If we're extending, we don't actually want to invoke any class
// construction logic. The above is sufficient to use this class in a
// prototype, so stop here.
if ( _self._extending )
if ( ClassInstance[ _priv ].extending )
{
return;
}
@ -968,7 +981,7 @@ exports.prototype.createAbstractCtor = function( cname )
var __abstract_self = function()
{
if ( !_self._extending )
if ( !__abstract_self[ _priv ].extending )
{
throw Error(
"Abstract class " + ( cname || '(anonymous)' ) +
@ -1255,18 +1268,23 @@ exports.prototype.attachStatic = function( ctor, members, base, inheriting )
/**
* Initializes class metadata for the given class
*
* DYNMETA is used only when CPARENT's metadata are flagged as "lazy",
* meaning that the data are not available at the time of its definition,
* but are available now as DYNMETA.
*
* @param {Function} func class to initialize metadata for
* @param {Function} cparent class parent
* @param {?Object} dynmeta dynamic metadata
*
* @return {undefined}
*
* Suppressed due to warnings for use of __cid
* @suppress {checkTypes}
*/
function createMeta( func, cparent )
function createMeta( func, cparent, dynmeta )
{
var id = func.__cid,
parent_meta = ( ( cparent.__cid )
parent_meta = ( cparent[ _priv ]
? exports.getMeta( cparent )
: undefined
);
@ -1274,7 +1292,13 @@ function createMeta( func, cparent )
// copy the parent prototype's metadata if it exists (inherit metadata)
if ( parent_meta )
{
return func[ _priv ].meta = util.clone( parent_meta, true );
return func[ _priv ].meta = util.clone(
// "lazy" metadata are unavailable at the time of definition
parent_meta._lazy
? dynmeta
: parent_meta,
true
);
}
// create empty
@ -1461,4 +1485,3 @@ function attachFlags( ctor, props )
// (v8 performance)
props.___$$final$$ = props.___$$abstract$$ = undefined;
}

View File

@ -97,8 +97,50 @@ function _createStaging( name )
}
Trait.extend = function( dfn )
Trait.extend = function( /* ... */ )
{
var an = arguments.length,
dfn = arguments[ an - 1 ],
has_ext_base = ( an > 1 ),
ext_base = ( has_ext_base ) ? arguments[ 0 ] : null;
if ( an > 2 )
{
throw Error(
"Unexpected number of arguments to Trait.extend"
);
}
if ( has_ext_base )
{
var basetype = typeof ext_base;
if ( ( ext_base === null )
|| !( ( basetype === 'object' )
|| ( basetype === 'function' )
) )
{
throw TypeError(
"Trait cannot extend base of type '" + basetype + "'"
);
}
// prevent extending final classes (TODO: abstract this check; see
// also ClassBuilder)
if ( ext_base.___$$final$$ === true )
{
throw TypeError(
"Trait cannot extend final class"
);
}
// TODO: this is intended to be temporary; see Trait/ClassExtendTest
if ( module.exports.isTrait( ext_base ) )
{
throw TypeError( "Traits cannot extend other traits" );
}
}
// we may have been passed some additional metadata
var meta = ( this || {} ).__$$meta || {};
@ -168,10 +210,11 @@ Trait.extend = function( dfn )
// and here we can see that traits are quite literally abstract classes
var tclass = base.extend( dfn );
Trait.__trait = type;
Trait.__acls = tclass;
Trait.__ccls = null;
Trait.toString = function()
Trait.__trait = type;
Trait.__acls = tclass;
Trait.__ccls = null;
Trait.__extbase = ext_base;
Trait.toString = function()
{
return ''+name;
};
@ -198,6 +241,41 @@ Trait.extend = function( dfn )
};
/**
* Validate whether mixin is permitted
*
* If a mixee (the trait being mixed in) extends some type S, then a
* contract has been created mandating that that trait may only be mixed
* into something of type S; a `TypeError` will be thrown if this contract
* is violated.
*
* @param {Class} base mixor (target of mixin)
* @param {Trait} T mixee (trait being mixed in)
*
* @return {undefined}
*
* @throws {TypeError} on type contract violation
*/
function _validateMixin( base, T )
{
if ( !T.__extbase )
{
return;
}
// TODO: isSubtypeOf
if ( !( ( T.__extbase === base )
|| ClassBuilder.isInstanceOf( T.__extbase, base.asPrototype() )
) )
{
throw TypeError(
"Cannot mix trait " + T.toString() + " into " + base.toString() +
"; mixor must be of type " + T.__extbase.toString()
);
}
}
/**
* Retrieve a string representation of the trait type
*
@ -530,12 +608,14 @@ function createVirtProxy( acls, dfn )
* @param {Trait} trait trait to mix in
* @param {Object} dfn definition object to merge into
* @param {Array} tc trait class context
* @param {Class} base target supertyep
* @param {Class} base target supertype
*
* @return {Object} dfn
*/
function mixin( trait, dfn, tc, base )
{
_validateMixin( base, trait );
// the abstract class hidden within the trait
var acls = trait.__acls;

View File

@ -34,6 +34,7 @@ var _console = ( typeof console !== 'undefined' ) ? console : undefined;
var util = require( './util' ),
ClassBuilder = require( './ClassBuilder' ),
Interface = require( './interface' ),
warn = require( './warn' ),
Warning = warn.Warning,
@ -179,11 +180,6 @@ module.exports.isClass = function( obj )
{
obj = obj || _dummyclass;
if ( !obj.prototype )
{
return false;
}
var meta = ClassBuilder.getMeta( obj );
// TODO: we're checking a random field on the meta object; do something
@ -477,6 +473,11 @@ function createImplement( base, ifaces, cname )
* be explicit: in this case, any instantiation attempts will result in an
* exception being thrown.
*
* This staging object may be used as a base for extending. Note, however,
* that its metadata are unavailable at the time of definition---its
* contents are marked as "lazy" and must be processed using the mixin's
* eventual metadata.
*
* @param {function()} basef returns base from which to lazily
* extend
* @param {Array.<Function>} traits traits to mix in
@ -491,6 +492,13 @@ function createUse( basef, traits, nonbase )
// invoking the partially applied class will immediately complete its
// definition and instantiate it with the provided constructor arguments
var partial = function()
{
return partialClass()
.apply( null, arguments );
};
var partialClass = function()
{
// this argument will be set only in the case where an existing
// (non-base) class is extended, meaning that an explict Class or
@ -503,8 +511,7 @@ function createUse( basef, traits, nonbase )
);
}
return createMixedClass( basef(), traits )
.apply( null, arguments );
return createMixedClass( basef(), traits );
};
@ -550,6 +557,20 @@ function createUse( basef, traits, nonbase )
return partial.extend( {} );
};
partial.asPrototype = function()
{
return partialClass().asPrototype();
};
partial.__isInstanceOf = Interface.isInstanceOf;
// allow the system to recognize this object as a viable base for
// extending, but mark the metadata as lazy: since we defer all
// processing for mixins, we cannot yet know all metadata
// TODO: `_lazy' is a kluge
ClassBuilder.masquerade( partial );
ClassBuilder.getMeta( partial )._lazy = true;
return partial;
}

View File

@ -24,7 +24,8 @@ require( 'common' ).testCase(
{
setUp: function()
{
this.Sut = this.require( 'class' );
this.Sut = this.require( 'class' );
this.ClassBuilder = this.require( 'ClassBuilder' );
this.Foo = this.Sut.extend(
{
@ -249,6 +250,21 @@ require( 'common' ).testCase(
},
/**
* There are cases---intended for internal use---where it is beneficial
* for an object to be treated as though it were actually a class.
*/
'Any object may masquerade as a class': function()
{
var obj = {};
// XXX: tightly coupled logic here; refactor things
this.ClassBuilder.masquerade( obj );
this.assertOk( this.Sut.isClass( obj ) );
},
/**
* This really should be encapsulated, probably, but it does exist for
* reference.

View File

@ -1,7 +1,7 @@
/**
* Tests abstract trait definition and use
*
* Copyright (C) 2014 Free Software Foundation, Inc.
* Copyright (C) 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -360,4 +360,70 @@ require( 'common' ).testCase(
_self.Class.use( Ta ).extend();
} );
},
/**
* Before traits, the only way to make an abstract class concrete, or
* vice versa, was by extending. Now, however, a mixing in a trait can
* introduce abstract or concrete methods. This poses a problem, since
* the syntax for providing self-documenting AbstractClass definitions
* no longer works: invoking `AbstractClass.use' produces different
* results than invoking `SomeAbstractClass.use', with the goal of
* extending it.
*
* Consider this issue: we wish to mix some trait T into abstract class
* AC. Trait T does not provide a concrete implementation of the
* abstract methods in AT, and so the resulting class after the final
* `#extend' call would be abstract.
*
* We have no choice but to allow extending the intermediate object
* produced by a class's `#use' method; otherwise, any call to `#extend'
* on the intermediate object would result in an error, because the
* class would still have abstract members, but has not been declared to
* be abstract. Handling abstract classes in this manner would be
* consistent with all other scenarios, and would be transparent: why
* should the user care that there is some odd intermediate object being
* used rather than an actual class?
*/
'Abstract classes can be derived from intermediates': function()
{
var chk = [{}];
var AC = this.AbstractClass( { 'abstract foo': [] } ),
T = this.Sut( { moo: function() { return chk; } } );
// mix trait into an abstract class
var M = this.AbstractClass.extend(
AC.use( T ),
{}
);
this.assertOk( this.Class.isClass( M ) );
this.assertOk( M.isAbstract() );
var inst = M.extend( { foo: function() {} } )();
// we should not have lost the original abstract class
this.assertOk(
this.Class.isA( AC, inst )
);
// not strictly necessary; comfort/sanity check: if this succeeds
// but the next fails, then there's a problem marking the
// implemented types
this.assertStrictEqual(
chk,
inst.moo()
);
// the trait should have been applied (see above note if this
// fails); if this does fail, note that, without
// AbstractClass.extend, we have (correctly):
// isA( T, AC.use( T ).extend( ... )() )
this.assertOk(
this.Class.isA( T, inst ),
'Instance is not recognized as having mixed in type T, but ' +
'incorporates its definition; metadata bug?'
);
},
} );

View File

@ -0,0 +1,279 @@
/**
* Tests extending traits from classes
*
* Copyright (C) 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
* ease.js is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require( 'common' ).testCase(
{
caseSetUp: function()
{
this.Sut = this.require( 'Trait' );
this.Class = this.require( 'class' );
this.AbstractClass = this.require( 'class_abstract' );
this.FinalClass = this.require( 'class_final' );
// nonsensical extend bases that do not support object
// representations (TODO: use some system-wide understanding of
// "extendable" values)
this.nonsense = [
null,
undefined,
false,
NaN,
Infinity,
-Infinity,
];
},
/**
* Normally, there are no restrictions on what class a trait may be
* mixed into. When ``extending'' a class, we would expect intuitively
* that this behavior would remain consistent.
*/
'Trait T extending class C can be mixed into C': function()
{
var C = this.Class( {} ),
T = this.Sut.extend( C, {} );
this.assertDoesNotThrow( function()
{
C.use( T )();
} );
},
/**
* Restrictions emerge once a disjoint type D attempts to mix in a trait
* T extending class C. When C is ``extended'', we are
* effectively extracting and implementing interfaces representing its
* public and protected members---this has all the same effects that one
* would expect from implementing an interface. However, the act of
* extension implies a tight coupling between T and C: we're not just
* expecting a particular interface; we're also expecting the mixee to
* behave in a certain manner, just as a subtype of C would expect.
*
* Traits extending classes therefore behave like conventional subtypes
* extending their parents, but with a greater degree of
* flexibility. We would not expect to be able to use a subtype of C as
* if it were a disjoint type D, because they are different types: even
* if they share an identical interface, their intents are
* distinct. This is the case here.
*/
'Trait T extending class C cannot be mixed into disjoint class D':
function()
{
var C = this.Class( {} ),
D = this.Class( {} ),
T = this.Sut.extend( C, {} );
this.assertThrows( function()
{
D.use( T )();
}, TypeError );
},
/**
* Just as some class D' extending supertype D is of both types D' and
* D, and a trait T implementing interface I is of both types T and I,
* we would expect that a trait T extending C would be of both types T
* _and_ C, since T is effectively implementing C's interface.
*/
'Trait T extending class C is of both types T and C': function()
{
var C = this.Class( {} ),
T = this.Sut.extend( C, {} ),
inst = C.use( T )();
this.assertOk( this.Class.isA( T, inst ) );
this.assertOk( this.Class.isA( C, inst ) );
},
/**
* Since a subtype C2 is, by definition, also of type C, we would expect
* that any traits that are valid to be mixed into type C would also be
* valid to be mixed into subtypes of C. This permits trait
* polymorphism in the same manner as classes and interfaces.
*/
'Trait T extending class C can be mixed into C subtype C2': function()
{
var C = this.Class( {} ),
C2 = C.extend( {} ),
T = this.Sut.extend( C, {} );
this.assertDoesNotThrow( function()
{
C2.use( T )();
} );
},
/**
* This is a corollary of the above associations.
*/
'Trait T extending subtype C2 cannot be mixed into supertype C':
function()
{
var C = this.Class( {} ),
C2 = C.extend( {} ),
T = this.Sut.extend( C2, {} );
this.assertThrows( function()
{
C.use( T )();
}, TypeError );
},
/**
* The trait `#extend' method mirrors the syntax of classes: the first
* argument is the class to be extended, and the second is the actual
* definition.
*/
'Trait definition can follow class extension': function()
{
var a = ['a'],
b = ['b'];
var C = this.Class( {
foo: function() { return a; }
} ),
T = this.Sut.extend( C, {
bar: function() { return b; }
} );
var inst = C.use( T )();
this.assertStrictEqual( inst.foo(), a );
this.assertStrictEqual( inst.bar(), b );
},
/**
* This is a corollary, but is still worth testing for assurance.
*
* We already stated that a trait Tb extending C's subtype C2 cannot be
* mixed into C, because C is not of type C2. But Ta extending C can be
* mixed into C2, because C2 _is_ of type C. Therefore, both of these
* traits should be able to co-mix in the latter situation, but not the
* former.
*/
'Trait Ta extending C and Tb extending C2 cannot co-mix': function()
{
var C = this.Class( 'C' ).extend( { _a: null } ),
C2 = this.Class( 'C2' ).extend( C, { _b: null } ),
Ta = this.Sut.extend( C, {} ),
Tb = this.Sut.extend( C2, {} );
// this is _not_ okay
this.assertThrows( function()
{
C.use( Ta ).use( Tb )();
} );
// but this is, since Tb extends C2 itself, and Ta extends C2's
// supertype
this.assertDoesNotThrow( function()
{
C2.use( Tb ).use( Ta )();
} );
},
/**
* The `#extend' method for traits, when extending a class, must not
* accept more than two arguments; otherwise, there may be a bug. It
* does not make sense to accept more arguments, since traits can only
* extend a single class.
*
* The reason? Well, as a corollary of the above, given types
* C_0,...,C_n to extend: C_x, 0<=x<n, must be equal to or a subtype of
* each C_i, 0<=xi<n, or the types are incompatible. In that case, the
* trait could just extend the subtype that has each other type C_i in
* its lineage, making multiple specifications unnecessary.
*
* Does that mean that it's not possible to combine two disjoint classes
* into one API that is a subtype of both? Yes, it does: that's
* multiple inheritance; use interfaces or traits, both of which are
* designed to solve this problem properly (the latter most closely).
*/
'Trait class extension cannot supply more than two arguments':
function()
{
var _self = this;
this.assertThrows( function()
{
// extra argument
_self.Sut.extend( _self.Class( {} ), {}, {} );
} );
},
/**
* Help out the programmer by letting her know when she provides an
* invalid base, which would surely not give her the result that she
* expects.
*/
'@each(nonsense) Traits cannot extend nonsense': function( base )
{
var _self = this;
this.assertThrows( function()
{
_self.Sut.extend( base, {} );
} );
},
/**
* Eventually, traits will be able to extend other traits just as they
* can classes---by asserting and operating on the type. This is just a
* generalization that needs to be properly tested and allowed, and
* should not function any differently than a class.
*
* Don't worry; it'll happen in the future.
*/
'Traits cannot yet extend other traits': function()
{
var _self = this;
this.assertThrows( function()
{
_self.Sut.extend( _self.Sut( {} ), {} );
}, TypeError );
},
/**
* For consistency with the rest of the system, final classes are not
* permitted to be extended.
*/
'Traits cannot extend final classes': function()
{
var _self = this;
this.assertThrows( function()
{
_self.Sut.extend( _self.FinalClass( {} ), {} );
}, TypeError );
},
} );

View File

@ -455,4 +455,24 @@ require( 'common' ).testCase(
Sut( dfn );
} );
},
/**
* The stating object rendered by `#use` calls implement the same
* methods as classes, and are even treated as classes when invoked
* using the immediate syntax (see ImmediateTest). When defining
* abstract classes, staging objects may be extended as if they were
* classes (see AbstractTest).
*
* It makes sense for staging objects to be able to be treated as if
* they were classes, which demands reflection API consistency.
*/
'Staging object for eventual mixin is considered to be class': function()
{
var T = this.Sut( {} );
this.assertOk(
this.Class.isClass( this.Class( {} ).use( T ) )
);
},
} );