Added section to manual on interoperable polymorphism
parent
ab04f5332f
commit
dd80997bea
237
doc/interop.texi
237
doc/interop.texi
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue