diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index 11bda6b..18795ec 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -72,6 +72,7 @@ var util = require( './util' ), */ public_methods = { '__construct': true, + '__mixin': true, 'toString': true, '__toString': true, }, diff --git a/lib/Trait.js b/lib/Trait.js index 4bd17c2..e140d2c 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -100,7 +100,9 @@ Trait.extend = function( dfn ) // store any provided name, since we'll be clobbering it (the definition // object will be used to define the hidden abstract class) - var name = dfn.__name || '(Trait)'; + var name = dfn.__name || '(Trait)', + type = _getTraitType( dfn ), + isparam = ( type === 'param' ); // augment the parser to handle our own oddities dfn.___$$parser$$ = { @@ -116,10 +118,41 @@ Trait.extend = function( dfn ) // give the abstract trait class a distinctive name for debugging dfn.__name = '#AbstractTrait#'; - function TraitType() - { - throw Error( "Cannot instantiate trait" ); - }; + // if __mixin was provided,in the definition, then we should crate a + // paramaterized trait + var Trait = ( isparam ) + ? function ParameterTraitType() + { + // duplicate ars in a way that v8 can optimize + var args = [], i = arguments.length; + while ( i-- ) args[ i ] = arguments[ i ]; + + var AT = function ArgumentTrait() + { + throw Error( "Cannot re-configure argument trait" ); + }; + + // TODO: mess! + AT.___$$mixinargs = args; + AT.__trait = 'arg'; + AT.__acls = Trait.__acls; + AT.__ccls = Trait.__ccls; + AT.toString = Trait.toString; + AT.__mixinImpl = Trait.__mixinImpl; + AT.__isInstanceOf = Trait.__isInstanceOf; + + // mix in the argument trait instead of the original + AT.__mixin = function( dfn, tc, base ) + { + mixin( AT, dfn, tc, base ); + }; + + return AT; + } + : function TraitType() + { + throw Error( "Cannot instantiate non-parameterized trait" ); + }; // implement interfaces if indicated var base = AbstractClass; @@ -131,33 +164,58 @@ Trait.extend = function( dfn ) // and here we can see that traits are quite literally abstract classes var tclass = base.extend( dfn ); - TraitType.__trait = true; - TraitType.__acls = tclass; - TraitType.__ccls = null; - TraitType.toString = function() + Trait.__trait = type; + Trait.__acls = tclass; + Trait.__ccls = null; + Trait.toString = function() { return ''+name; }; // invoked to trigger mixin - TraitType.__mixin = function( dfn, tc, base ) - { - mixin( TraitType, dfn, tc, base ); - }; + Trait.__mixin = ( isparam ) + ? function() + { + throw TypeError( + "Cannot mix in parameterized trait " + Trait.toString() + + "; did you forget to configure it?" + ); + } + : function( dfn, tc, base ) + { + mixin( Trait, dfn, tc, base ); + }; // mixes in implemented types - TraitType.__mixinImpl = function( dest_meta ) + Trait.__mixinImpl = function( dest_meta ) { mixinImpl( tclass, dest_meta ); }; // TODO: this and the above should use util.defineSecureProp - TraitType.__isInstanceOf = Interface.isInstanceOf; + Trait.__isInstanceOf = Interface.isInstanceOf; - return TraitType; + return Trait; }; +/** + * Retrieve a string representation of the trait type + * + * A trait is parameterized if it has a __mixin method; otherwise, it is + * standard. + * + * @param {Object} dfn trait definition object + * @return {string} trait type + */ +function _getTraitType( dfn ) +{ + return ( typeof dfn.__mixin === 'function' ) + ? 'param' + : 'std'; +} + + /** * Verifies trait member restrictions * @@ -295,8 +353,7 @@ function createImplement( ifaces, name ) /** * Determines if the provided value references a trait * - * @param {*} trait value to check - * + * @param {*} trait value to check * @return {boolean} whether the provided value references a trait */ Trait.isTrait = function( trait ) @@ -305,6 +362,32 @@ Trait.isTrait = function( trait ) }; +/** + * Determines if the provided value references a parameterized trait + * + * @param {*} trait value to check + * @return {boolean} whether the provided value references a param trait + */ +Trait.isParameterTrait = function( trait ) +{ + return !!( ( trait || {} ).__trait === 'param' ); +}; + + +/** + * Determines if the provided value references an argument trait + * + * An argument trait is a configured parameter trait. + * + * @param {*} trait value to check + * @return {boolean} whether the provided value references an arg trait + */ +Trait.isArgumentTrait = function( trait ) +{ + return !!( ( trait || {} ).__trait === 'arg' ); +}; + + /** * Create a concrete class from the abstract trait class * @@ -670,6 +753,8 @@ function addTraitInst( T, dfn, tc, base ) * resulting objects assigned to their rsepective pre-determined field * names. * + * The MIXINARGS are only useful in the case of parameterized trait. + * * This will lazily create the concrete trait class if it does not already * exist, which saves work if the trait is never used. * @@ -703,6 +788,9 @@ function tctor( tc, base, privsym ) this[ f ] = C( base, this[ privsym ].vis )[ privsym ].vis; } + // this has been previously validated to ensure that it is a function + this.__mixin && this.__mixin.apply( this, T.___$$mixinargs ); + // if we are a subtype, be sure to initialize our parent's traits this.__super && this.__super( privsym ); }; diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js index 5f14802..df6ba8f 100644 --- a/test/Trait/DefinitionTest.js +++ b/test/Trait/DefinitionTest.js @@ -82,6 +82,8 @@ require( 'common' ).testCase( /** * A trait can only be used by something else---it does not make sense * to instantiate them directly, since they form an incomplete picture. + * + * Now, that said, see parameterized traits. */ '@each(ctor) Cannot instantiate trait without error': function( T ) { diff --git a/test/Trait/ParameterTest.js b/test/Trait/ParameterTest.js new file mode 100644 index 0000000..a4e87cb --- /dev/null +++ b/test/Trait/ParameterTest.js @@ -0,0 +1,211 @@ +/** + * Tests parameterized traits + * + * Copyright (C) 2014 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 . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + + var _self = this; + this.createParamTrait = function( f ) + { + return _self.Sut( { __mixin: ( f || function() {} ) } ); + }; + }, + + + /** + * Since traits are reusable components mixed into classes, they + * themselves do not have a constructor. This puts the user at a + * disadvantage, because she would have to create a new trait to simply + * to provide some sort of configuration at the time the class is + * instantiated. Adding a method to do the configuration is another + * option, but that is inconvenient, especially when the state is + * intended to be immutable. + * + * This does not suffer from the issue that Scala is having in trying to + * implement a similar feature because traits cannot have non-private + * properties; the linearization process disambiguates. + * + * When a trait contains a __mixin method, it is created as a + * ParameterTraitType instead of a TraitType. Both must be recognized as + * traits so that they can both be mixed in as expected; a method is + * provided to assert whether or not a trait is a parameter trait + * programatically, since attempting to configure a non-param trait will + * throw an exception. + */ + 'Can create parameter traits': function() + { + var T = this.createParamTrait(); + + this.assertOk( this.Sut.isParameterTrait( T ) ); + this.assertOk( this.Sut.isTrait( T ) ); + }, + + + /** + * A parameter trait is in an uninitialized state---it cannot be mixed + * in until arguments have been provided; same rationale as a class + * constructor. + */ + 'Cannot mix in a parameter trait': function() + { + var _self = this; + this.assertThrows( function() + { + _self.Class.use( _self.createParamTrait() )(); + } ); + }, + + + /** + * Invoking a parameter trait will produce an argument trait which may + * be mixed in. This has the effect of appearing as though the trait is + * being instantiated (but it's not). + */ + 'Invoking parameter trait produces argument trait': function() + { + var _self = this; + this.assertDoesNotThrow( function() + { + _self.assertOk( + _self.Sut.isArgumentTrait( _self.createParamTrait()() ) + ); + } ); + }, + + + /** + * Traits cannot be instantiated; ensure that this remains true, even + * with the parameterized trait implementation. + */ + 'Invoking a standard trait throws an exception': function() + { + var Sut = this.Sut; + this.assertThrows( function() + { + // no __mixin method; not a param trait + Sut( {} )(); + } ); + }, + + + /** + * Argument traits can be mixed in just as non-parameterized traits can; + * it would be silly not to consider them to be traits through our + * reflection API. + */ + 'Recognizes argument trait as a trait': function() + { + this.assertOk( + this.Sut.isTrait( this.createParamTrait()() ) + ); + }, + + + /** + * A param trait, upon configuration, returns an immutable argument + * trait; any attempt to invoke it (e.g. to try to re-configure) is in + * error. + */ + 'Cannot re-configure argument trait': function() + { + var _self = this; + this.assertThrows( function() + { + // ParameterTrait => ArgumentTrait => Error + _self.createParamTrait()()(); + } ); + }, + + + /** + * Upon instantiating a class into which an argument trait was mixed, + * all configuration arguments should be passed to the __mixin method. + * Note that this means that __mixin *will not* be called at the point + * that the param trait is configured. + */ + '__mixin is invoked upon class instantiation': function() + { + var called = 0; + var T = this.createParamTrait( function() + { + called++; + } ); + + // ensure we only invoke __mixin a single time + this.Class( {} ).use( T() )(); + this.assertEqual( called, 1 ); + }, + + + /** + * Standard sanity check---make sure that the arguments provided during + * configuration are passed as-is, by reference, to __mixin. Note that + * this has the terrible consequence that, should one of the arguments + * be modified by __mixin (e.g. an object field), then it will be + * modified for all other __mixin calls. But that is the case with any + * function. ;) + */ + '__mixin is passed arguments by reference': function() + { + var args, + a = { a: 'a' }, + b = { b: 'b' }; + + var T = this.createParamTrait( function() + { + args = arguments; + } ); + + this.Class( {} ).use( T( a, b ) )(); + + this.assertStrictEqual( a, args[ 0 ] ); + this.assertStrictEqual( b, args[ 1 ] ); + }, + + + /** + * The __mixin method should be invoked within the context of the trait + * and should therefore have access to its private members. Indeed, + * parameterized traits would have far more limited use if __mixin did + * not have access to private members, because that would be the proper + * place to hold configuration data. + */ + '__mixin has access to trait private members': function() + { + var expected = {}; + + var T = this.Sut( + { + 'private _foo': null, + __mixin: function( arg ) { this._foo = arg; }, + getFoo: function() { return this._foo; }, + } ); + + this.assertStrictEqual( expected, + this.Class( {} ).use( T( expected ) )().getFoo() + ); + }, +} ); +