Strawman proposal: new `is` operator
On Sun, Aug 24, 2014 at 4:04 PM, Isiah Meadows <impinball at gmail.com> wrote:
There is no way currently to reliably verify types in JS. Type annotations cannot solve this completely, especially when you have an overloaded function. A way to reliably verify types would make this far easier. So far, here are the current options with their respective caveats:
instanceof
- This returns true for the class, but also for subclasses. This isn't always desired.
Your proposal fixes this, but does not address the other issues you raise:
this.constructor === Foo
- The constructor property could easily be overwritten, rendering this relatively useless.
Object.getPrototypeOf(this) === Foo.prototype
is the test which
instanceof
uses.
Does not require new syntax.
typeof
- This only is useful for literals, with the exception ofnull
.
Your proposed is
operator is not useful for literals.
Object.prototype.toString.call()
- This returns the correct native object type for native objects, but returns[object Object]
for all non-native objects, rendering it effectively useless. This includes object instances created fromclass Foo {}
, etc.
In general, you can always define:
Object.prototype.is = function(constructor) {
return Object.getPrototypeOf(this) === constructor.prototype;
};
rather than try to use toString. Unless you want it to work on literals...
On Sun, 24 Aug 2014, Isiah Meadows wrote:
One big question I have is whether a different keyword should be used (
isa
, etc), given its use as a keyword synonym to===
in many compile-to-JS languages, most notably CoffeeScript.
"is" is used to mean "is an instance of or an instance of a subclass of" in C# and modern Pascals, FWIW. Perl uses "isa" for this.
Python, Coffeescript and VB use the "is" operator for object identity, not for instanceof like functionality. Dart uses the is operator analogous to instanceof. It would seem to me it'd be beneficial to pick a different operator name, in order to avoid the fuddled meaning this operator has taken to mean in a variety of languages.
I know it's just a sketch but that Object.prototype.is
does not play well
with Object.create
where no constructor is necessary to create is
relations.
Cc the list...
Isiah Meadows wrote:
Cc the list...
On Aug 25, 2014 6:06 PM, "Isiah Meadows" <impinball at gmail.com <mailto:impinball at gmail.com>> wrote:
There really shouldn't be any sort of object construction needed to check types like this. `isa` may be better, anyways, but I still find that requirement to build and destroy an object to check somewhat counterproductive.
What object is built and destroyed?
Note that for primitive types no wrapper need be created just to call a method, in general for JIT-optimized code, and definitely for strict mode code.
Andrea gets what I'm talking about. Also, another (possibly separate) proposal would be to make cases like `"foo" instanceof String" === true` instead of their current behavior, throwing a TypeError. (I believe...I'm on a phone, not a PC where I can test. Correct me if I'm wrong.)
(Your phone doesn't have a browser with a console? :-P)
js> "hi" instanceof String
false js> 42 instanceof Number
false js> false instanceof Boolean
false
These are well-defined for instanceof, without throwing. Changing results to true would be backward incompatible.
ES4 had 'is' as a type-classifying operator:
proposals:is_as_to, discussion:is_as_to
It would be a mistake to define 'is' without defining the (unsound) type system it depends on. This is a challenge, but TypeScript and other close-to-ES6 languages have sallied forth. We need a detailed proposal.
On Tue, Aug 26, 2014 at 3:56 AM, Brendan Eich <brendan at mozilla.org> wrote:
ES4 had 'is' as a type-classifying operator:
proposals:is_as_to, discussion:is_as_to
It would be a mistake to define 'is' without defining the (unsound) type system it depends on. This is a challenge, but TypeScript and other close-to-ES6 languages have sallied forth. We need a detailed proposal.
Though TypeScript cannot support an is
operator because it only has duck
typing/structural types. Which makes sense for the web, I think, given that
lots of code has to interact across realms, and staticly-checked types with
their inherent anti-modularity are a problem for that. AS3 (which
implements ES4's is
operator) has the same issue across Flash's
ApplicationDomains.
Note that ES6's formulation of instanceof
in terms of an optional
@@hasInstance
trap can, in combination with the cross-realm symbol
registry, probably be used to implement the desired behavior, even across
realms. For non-primitives, at least.
I stand corrected on the creation aspect.
Isiah Meadows
Here's my (more formalized) proposition. I also have added a proposed
@@isInstance property for the isa
equivalent for @@hasInstance. I know
this was mostly put on hold initially because of my lack of proper
formulation, but here it is.
Rationale:
Include an isa
operator to determine if an object is a direct instance of
a constructor, and not simply a child constructor. It is also meant to work
with native primitives as well, which the instanceof
operator lacks. It
helps when child constructors are not necessarily known, and a method specific
to the parent is made (there is no way currently to determine this) or when
implementing an abstract class that defines methods that must be overridden
for all subclasses. Example usage:
// Example 1:
class Foo {
constructor() {
if (this isa Foo) throw new TypeError('Abstract class')
}
}
class Bar extends Foo {}
let foo = new Foo(); // Error!
let bar = new Bar(); // Good
/****************************************************************************/
// Example 2:
class Foo {
constructor() {}
doSomething() {
if (!(this isa Foo)) {
throw new TypeError('Must be a Foo instance');
}
// do stuff
}
}
class Bar extends Foo {}
let bar = new Bar();
bar.doSomething(); // Error!
/****************************************************************************/
// Example 3: (adapted from old code)
const assert = (obj) => {
let isInstance = (b) => { if (!(a instanceof b)) throw new TypeError() };
let isType = (b) => { if (!(a isa b)) throw new TypeError() };
return {
isInstance: isInstance,
isType: isType,
};
};
// ...
class WhileStatement extends Statement {
constructor(condition, body, label = '') {
super();
assert(condition).isInstance(Expression);
assert(body).isType(Array);
body.forEach((i) => assert(i).isInstance(Expression));
assert(label).isType(String); // native string
this.condition = condition;
this.body = body;
this.label = label;
}
}
// ...
class Foo extends String {
constructor(val) {
super(val);
}
}
new WhileStatement(expr, [...elements], 'label') // Good
new WhileStatement(expr, [...elements]) // Good
new WhileStatement(stmt, [...elements], 'label') // Error!
new WhileStatement(expr, {}, 'label') // Error!
new WhileStatement(expr, typedArray, 'label') // Error! (before iterating)
new WhileStatement(expr, funcArray, 'label') // Error!
/* Currently cannot make this throw */
new WhileStatement(expr, [...elements], new Foo('label')) // Error! (desired)
/****************************************************************************/
// Example 4: (Example 3 using Node builtins)
class WhileStatement extends Statement {
constructor(condition, body, label = '') {
super();
assert(condition instanceof Expression);
assert(body isa Array);
body.forEach((i) => assert(i instanceof Expression));
assert(label isa String); // native string
this.condition = condition;
this.body = body;
this.label = label;
}
}
// ...
class Foo extends String {
constructor(val) {
super(val);
}
}
new WhileStatement(expr, [...elements], 'label') // Good
new WhileStatement(expr, [...elements]) // Good
new WhileStatement(stmt, [...elements], 'label') // Error!
new WhileStatement(expr, {}, 'label') // Error!
new WhileStatement(expr, typedArray, 'label') // Error! (before iterating)
new WhileStatement(expr, funcArray, 'label') // Error!
/* Currently cannot make this throw */
new WhileStatement(expr, [...elements], new Foo('label')) // Error! (desired)
This already exists in some form in several languages, such as Ruby. It would
be great for unit testing, library API type checking (e.g.
options isa Object
, options isa Map
, code isa String
), and it may lay
some solid groundwork for
this proposition to
more easily get off the ground (internal implementation will become relatively
easy).
Well-known Symbols:
Add this row to the table.
<!-- Apologies to those of you reading this text-only:
Please pardon my HTML...it's the only way I could get this to display right otherwise. I apologize...I've been using a text editor to type all this crap, with no more than automatic indention, brace detection and syntax highlighting. :(
I have made it a point to keep each line to a maximum of 79 columns so it can still be easily read in emacs/etc. in a popup terminal. -->
<table> <thead> <th>Specification Name</th> <th>[[Description]]</th> <th>Value and Purpose</th> </thead> <tbody> <tr> <td style="text-align:center">...</td> <td style="text-align:center">...</td> <td style="text-align:center">...</td> </tr> <tr> <td>@@isInstance</td> <td>"Symbol.isInstance"</td> <td style="width:40%">
A method that determines if a constructor object recognizes an object or
primitive as of the same type as the constructor. Called by the semantics
of the isa
operator.
</td>
</tr>
<tr>
<td style="text-align:center">...</td>
<td style="text-align:center">...</td>
<td style="text-align:center">...</td>
</tr>
</table>
Syntax:
RelationalExpression:
RelationalExpression<sub>[?In, ?Yield]</sub>
isa
ShiftExpression <sub>[?Yield]</sub>
Static Semantics: IsFunctionDefinition
RelationalExpression:
RelationalExpression
isa
ShiftExpression
- Return false.
Static Semantics: IsValidSimpleTarget
RelationalExpression:
RelationalExpression
isa
ShiftExpression
- Return false.
Runtime Semantics: Evaluation
RelationalExpression
isa
ShiftExpression
-
Let lref be the result of evaluating RelationalExpression.
-
Let lval be GetValue(lref).
-
ReturnIfAbrupt(lval)
-
Let rref be the result of evaluating ShiftExpression.
-
Let rval be GetValue(rref).
-
ReturnIfAbrupt(rval).
-
Return IsaOperator(lval, rval).
Abstract Operation: IsaOperator(O, C)
-
If Type(C) is not Object, throw a TypeError exception.
-
Let isaHandler be GetMethod(@@isInstance)
-
ReturnIfAbrupt(isaHandler)
-
If isaHandler is not
undefined
, then -
Let result be the result of calling the [[Call]] internal method of isaHandler with C passed as thisArgument and a new List containing O as its argumentsList.
-
Return ToBoolean(result).
-
If O is a primitive value, then
-
If IsPrimitiveConstructor(C, O), return true.
-
Return false.
-
If IsCallable(C) is false, throw a TypeError exception.
-
Return IsInstance(C, O).
Abstract Operation: IsInstance(C, O)
-
If IsCallable(C) is false, return false.
-
If C has a [[BoundTargetFunction]] internal slot, then
-
Let BC be the value of C's [[BoundTargetFunction]] internal slot.
-
Return IsaOperator(O, BC).
-
If Type(O) is not Object, return false.
-
Let P be Get(C,
"prototype"
). -
ReturnIfAbrupt(P).
-
If Type(P) is not Object, throw a TypeError exception.
-
Set O to the result of calling the [[GetPrototypeOf]] internal method of O with no arguments.
-
ReturnIfAbrupt(O).
-
If O is
null
, return false. -
If SameValue(O) is true, return true.
-
Return false.
Abstract Operation: IsPrimitiveConstructor(C, O)
- If C is not the same Object value as one of the following intrinsic constructors, return false:
-
%Boolean%
-
%Number%
-
%String%
-
%Symbol%
-
If Type(O) and C are the same Object value, return true.
-
Return false.
There is no way currently to reliably verify types in JS. Type annotations cannot solve this completely, especially when you have an overloaded function. A way to reliably verify types would make this far easier. So far, here are the current options with their respective caveats:
instanceof
- This returns true for the class, but also for subclasses. This isn't always desired.this.contructor === Foo
- The constructor property could easily be overwritten, rendering this relatively useless.typeof
- This only is useful for literals, with the exception ofnull
.Object.prototype.toString.call()
- This returns the correct native object type for native objects, but returns[object Object]
for all non-native objects, rendering it effectively useless. This includes object instances created fromclass Foo {}
, etc.This proposed operator,
is
, would basically use the same algorithm for theinstanceof
operator, but instead of walking up the prototype chain iteratively, it would only check the first level of the right side. The static and runtime semantics would be equal to that of theinstanceof
operator.It would allow for simpler overloading, simpler assertions, simpler type checks, and simpler testing.
Example usage:
One big question I have is whether a different keyword should be used (
isa
, etc), given its use as a keyword synonym to===
in many compile-to-JS languages, most notably CoffeeScript.