1
0
Fork 0

Added section to manual on interoperable polymorphism

newmaster
Mike Gerwitz 2014-04-27 02:09:49 -04:00
parent ab04f5332f
commit dd80997bea
1 changed files with 237 additions and 0 deletions

View File

@ -20,6 +20,7 @@ have adopted ease.js for your own projects.
@menu @menu
* Using GNU ease.js Classes Outside of ease.js:: * Using GNU ease.js Classes Outside of ease.js::
* Prototypally Extending Classes:: * Prototypally Extending Classes::
* Interoperable Polymorphism::
@end menu @end menu
@ -128,3 +129,239 @@ chain:
ease.js normally prevents this from happening automatically. ease.js normally prevents this from happening automatically.
@end itemize @end itemize
@node Interoperable Polymorphism
@section Interoperable Polymorphism
GNU ease.js encourages polymorphism through type checking. In the case of
@ref{Prototypally Extending Classes,,prototypal subtyping}, type checks will
work as expected:
@float Figure, f:typecheck-protosub
@verbatim
var Foo = Class( {} );
function SubFoo() {};
SubFoo.prototype = Foo.asPrototype();
SubFoo.constructor = Foo;
var SubSubFoo = Class.extend( SubFoo, {} );
// vanilla ECMAScript
( new Foo() ) instanceof Foo; // true
( new Subfoo() ) instanceof Foo; // true
( new SubSubFoo() ) instanceof Foo; // true
( new SubSubFoo() ) instanceof SubFoo; // true
// GNU ease.js
Class.isA( Foo, ( new Foo() ) ); // true
Class.isA( Foo, ( new SubFoo() ) ); // true
Class.isA( Foo, ( new SubSubFoo() ) ); // true
Class.isA( SubFoo, ( new SubSubFoo() ) ); // true
@end verbatim
@caption{Type checking with prototypal subtypes of GNU ease.js classes}
@end float
Plainly---this means that prototypes that perform type checking for
polymorphism will accept GNU ease.js classes and vice versa. But this is not
the only form of type checking that ease.js supports.
This is the simplest type of polymorphism and is directly compatible with
ECMAScript's prototypal mode. However, GNU ease.js offers other features
that are alien to ECMAScript on its own.
@menu
* Interface Interop:: Using GNU ease.js interfaces in conjunction with
vanilla ECMAScript
@end menu
@node Interface Interop
@subsection Interface Interop
@ref{Interfaces}, when used within the bounds of GNU ease.js, allow for
strong typing of objects. Further, two interfaces that share the same API
are not equivalent; this permits conveying intent: Consider two interfaces
@code{Enemy} and @code{Toad}, each defining a method @code{croak}. The
method for @code{Enemy} results in its death, whereas the method for
@code{Toad} produces a bellowing call. Clearly classes implementing these
interfaces will have different actions associated with them; we would
probably not want an invincible enemy that croaks like a toad any time you
try to kill it (although that'd make for amusing gameplay).
@float figure, f:interface-croak
@verbatim
var Enemy = Interface( { croak: [] } ),
Toad = Interface( { croak: [] } ),
AnEnemy = Class.implement( Enemy ).extend( /*...*/ ),
AToad = Class.implement( Toad ).extend( /*...*/ );
// GNU ease.js does not consider these interfaces to be equivalent
Class.isA( Enemy, AnEnemy() ); // true
Class.isA( Toad, AnEnemy() ); // false
Class.isA( Enemy, AToad() ); // false
Class.isA( Toad, AToad() ); // true
defeatEnemy( AnEnemy() ); // okay; is an enemy
defeatEnemy( AToad() ); // error; is a toad
function defeatEnemy( enemy )
{
if ( !( Class.isA( Enemy, enemy ) ) ) {
throw TypeError( "Expecting enemy" );
}
enemy.croak();
}
@end verbatim
@caption{Croak like an enemy or a toad?}
@end float
In JavaScript, it is common convention to instead use @emph{duck typing},
which does not care what the intent of the interface is---it merely cares
whether the method being invoked actually exists.@footnote{``When I see a
bird that walks like a duck and swims like a duck and quacks like a duck, I
call that bird a duck.'' (James Whitcomb Riley).} So, in the case of the
above example, it is not a problem that an toad may be used in place of an
enemy---they both implement @code{croak} and so @emph{something} will
happen. This is most often exemplified by the use of object literals to
create ad-hoc instances of sorts:
@float figure, f:interface-objlit
@verbatim
var enemy = { croak: function() { /* ... */ ) },
toad = { croak: function() { /* ... */ ) };
defeatEnemy( enemy ); // okay; duck typing
defeatEnemy( toad ); // okay; duck typing
// TypeError: object has no method 'croak'
defeatEnemy( { moo: function() { /*...*/ } } );
function defeatEnemy( enemy )
{
enemy.croak();
}
@end verbatim
@caption{Duck typing with object literals}
@end float
Duck typing has the benefit of being ad-hoc and concise, but places the onus
on the developer to realize the interface and ensure that it is properly
implemented. Therefore, there are two situations to address for GNU ease.js
users that prefer strongly typed interfaces:
@enumerate
@item
Ensure that non-ease.js users can create objects acceptable to the
strongly-typed API; and
@item
Allow ease.js classes to require a strong API for existing objects.
@end enumerate
These two are closely related and rely on the same underlying concepts.
@menu
* Object Interface Compatibility:: Using vanilla ECMAScript objects where
type checking is performed on GNU ease.js
interfaces
* Building Interfaces Around Objects:: Using interfaces to validate APIs of
ECMAScript objects
@end menu
@node Object Interface Compatibility
@subsubsection Object Interface Compatibility
It is clear that GNU ease.js' distinction between two separate interfaces
that share the same API is not useful for vanilla ECMAScript objects,
because those objects do not have an API for implementing interfaces (and if
they did, they wouldn't be ease.js' interfaces). Therefore, in order to
design a transparently interoperable system, this distinction must be
removed (but will be @emph{retained} within ease.js' system).
The core purpose of an interface is to declare an expected API, providing
preemptive warnings and reducing the risk of runtime error. This is in
contrast with duck typing, which favors recovering from errors when (and if)
they occur. Since an ECMAScript object cannot implement an ease.js interface
(if it did, it'd be using ease.js), the conclusion is that ease.js should
fall back to scanning the object to ensure that it is compatible with a
given interface.
A vanilla ECMAScript object is compatible with an ease.js interface if it
defines all interface members and meets the parameter count requirements of
those members.
@float Figure, f:interface-compat
@verbatim
var Duck = Interface( {
quack: [ 'str' ],
waddle: [],
} );
// false; no quack
Class.isA( Duck, { waddle: function() {} } );
// false; quack requires one parameter
Class.isA( Duck, {
quack: function() {},
waddle: function() {},
} );
// true
Class.isA( Duck, {
quack: function( str ) {},
waddle: function() {},
} );
// true
function ADuck() {};
ADuck.prototype = {
quack: function( str ) {},
waddle: function() {},
};
Class.isA( Duck, ( new ADuck() ) );
@end verbatim
@caption{Vanilla ECMAScript object interface compatibility}
@end float
@node Building Interfaces Around Objects
@subsubsection Building Interfaces Around Objects
A consequence of @ref{Object Interface Compatibility,,the previous section}
is that users of GNU ease.js can continue to use strongly typed interfaces
even if the objects they are interfacing with do not support ease.js'
interfaces. Consider, for example, a system that uses @code{XMLHttpRequest}:
@float Figure, f:interface-xmlhttp
@verbatim
// modeled around XMLHttpRequest
var HttpRequest = Interface(
{
abort: [],
open: [ 'method', 'url', 'async', 'user', 'password' ],
send: [],
} );
var FooApi = Class(
{
__construct: function( httpreq )
{
if ( !( Class.isA( HttpRequest, httpreq ) ) )
{
throw TypeError( "Expecting HttpRequest" );
}
// ...
}
} );
FooApi( new XMLHttpRequest() ); // okay
@end verbatim
@caption{Building an interface around needed functionality of
XMLHttpRequest}
@end float
This feature permits runtime polymorphism with preemptive failure instead of
inconsistently requiring duck typing for external objects, but interfaces for
objects handled through ease.js.