1
0
Fork 0

Initial implementation of parameterized traits

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.
textend
Mike Gerwitz 2014-05-28 23:37:36 -04:00
parent a266cfe91b
commit 3fc0f90e01
4 changed files with 320 additions and 18 deletions

View File

@ -72,6 +72,7 @@ var util = require( './util' ),
*/ */
public_methods = { public_methods = {
'__construct': true, '__construct': true,
'__mixin': true,
'toString': true, 'toString': true,
'__toString': true, '__toString': true,
}, },

View File

@ -100,7 +100,9 @@ Trait.extend = function( dfn )
// store any provided name, since we'll be clobbering it (the definition // store any provided name, since we'll be clobbering it (the definition
// object will be used to define the hidden abstract class) // 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 // augment the parser to handle our own oddities
dfn.___$$parser$$ = { dfn.___$$parser$$ = {
@ -116,9 +118,40 @@ Trait.extend = function( dfn )
// give the abstract trait class a distinctive name for debugging // give the abstract trait class a distinctive name for debugging
dfn.__name = '#AbstractTrait#'; dfn.__name = '#AbstractTrait#';
function TraitType() // if __mixin was provided,in the definition, then we should crate a
// paramaterized trait
var Trait = ( isparam )
? function ParameterTraitType()
{ {
throw Error( "Cannot instantiate trait" ); // 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 // implement interfaces if indicated
@ -131,33 +164,58 @@ Trait.extend = function( dfn )
// and here we can see that traits are quite literally abstract classes // and here we can see that traits are quite literally abstract classes
var tclass = base.extend( dfn ); var tclass = base.extend( dfn );
TraitType.__trait = true; Trait.__trait = type;
TraitType.__acls = tclass; Trait.__acls = tclass;
TraitType.__ccls = null; Trait.__ccls = null;
TraitType.toString = function() Trait.toString = function()
{ {
return ''+name; return ''+name;
}; };
// invoked to trigger mixin // invoked to trigger mixin
TraitType.__mixin = function( dfn, tc, base ) Trait.__mixin = ( isparam )
? function()
{ {
mixin( TraitType, dfn, tc, base ); 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 // mixes in implemented types
TraitType.__mixinImpl = function( dest_meta ) Trait.__mixinImpl = function( dest_meta )
{ {
mixinImpl( tclass, dest_meta ); mixinImpl( tclass, dest_meta );
}; };
// TODO: this and the above should use util.defineSecureProp // 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 * Verifies trait member restrictions
* *
@ -296,7 +354,6 @@ function createImplement( ifaces, name )
* Determines if the provided value references a trait * 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 * @return {boolean} whether the provided value references a trait
*/ */
Trait.isTrait = function( 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 * 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 * resulting objects assigned to their rsepective pre-determined field
* names. * 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 * This will lazily create the concrete trait class if it does not already
* exist, which saves work if the trait is never used. * 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[ 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 // if we are a subtype, be sure to initialize our parent's traits
this.__super && this.__super( privsym ); this.__super && this.__super( privsym );
}; };

View File

@ -82,6 +82,8 @@ require( 'common' ).testCase(
/** /**
* A trait can only be used by something else---it does not make sense * A trait can only be used by something else---it does not make sense
* to instantiate them directly, since they form an incomplete picture. * 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 ) '@each(ctor) Cannot instantiate trait without error': function( T )
{ {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
);
},
} );