need some clarification on compile-time type vs. run-time type
Yuh-Ruey Chen
maian330 at gmail.com
Sat Nov 10 21:03:47 PST 2007
Brendan Eich wrote:
> On Nov 9, 2007, at 11:24 PM, Yuh-Ruey Chen wrote:
> > Ah, so expressions can be annotated with types?
>
> Array and object initialisers can be annotated with array and object
> ("record") structural types to make the expressed object have fixed
> properties named by the type, instead of just being a plain old Array
> or Object instance that has ad-hoc ("expando") properties given by
> the initialiser, where the properties could be deleted in the very
> next statement.
>
I see. I wonder if this can somehow be extended so that it works on
non-structural types as well. For example, to initialize a Map, you
could use {a: 0, b: 1, c: 2} : Map, instead of Map({a:0, b:1, c:2}).
Perhaps it would map onto the |meta static function invoke| method. The
downside of this syntax sugar though, is that it hides the cost of the
creation of the object literal, fooling the user into thinking that the
Map is directly initialized from the literal.
> > I acknowledged that |is| is different from |instanceof| but I was
> > using
> > |instanceof| because it is similar to |is| yet works on value exprs. A
> > more proper example would be a combination of |instanceof| and all the
> > other checks that |is| allows via reflection.
>
> Right. You can see from:
>
> http://wiki.ecmascript.org/doku.php?
> id=proposals:syntax_for_type_expressions
>
> and Graydon's reply that we originally had 'is' take a value
> expression right operand, and you had to use 'type E' (the unary type
> operator mentioned at the bottom of the above-linked page) to force E
> to be treated as a type expression. We've moved away from that to get
> experience with the alternative design, where 'is' takes a type
> expression on the right, in order to get earlier and more certain
> type checking. But this is all subject to change based on feedback --
> such as yours ;-).
>
I like this revised syntax: |a is type b| is much clearer than |a is b|,
which a newbie can mistake for object equality, especially if he comes
from a Python background. On purely aesthetic grounds though (ignoring
the type vs. value expr distinction), I think |isa| would be even
better, but on the other hand, |isa like| sounds like something Mario
would say.
Can you elaborate on the "earlier and more certain type checking"? I
thought you said for |is| checks (along with |instanceof| checks) there
can be no early type errors, so that leaves |is| to be a purely runtime
check.
Anyway, we still have a similar confusion to what we had with
|instanceof|, except this time it's all in |is|:
// assume |b| is a class and |a| is an instance of it
a is type b
a is b
Since both work, this can fool the user into thinking that the |type|
operator is optional. Furthermore, the latter can still lead to newbie
confusion (thinking that the expr is equivalent to |a == b|). So I'm not
sure there is a net win.
> > Again, I did not mean to use the strict definition of |instanceof|; I
> > meant some abstract operator (which |instanceof| could be
> > "upgraded" to)
> > that does the same thing as |is| except that it works on value exprs.
>
> It's not clear (to me at any rate) that you can upgrade instanceof in
> a backward-compatible fashion, given mutable prototype properties of
> functions.
>
> For the built-ins (which become classes, not constructor functions),
> you could, and that's what we've done.
>
> For user-defined functions that instanceof takes as right operands,
> there's no need to upgrade beyond how things work as in ES3. But the
> guarantees with classes are gone: if one changes F.prototype after x
> = new F, (x instanceof F) could change from true to false.
>
Well, I was just suggesting adding additional functionality to
|instanceof|, not changing the original behavior of it, so I'm not sure
where the compatibility issue is coming into play.
> > If |instanceof| works for classes, then I propose that |instanceof|
> > also
> > work for interfaces for the sake of completeness. Getting this to work
> > is a bit trickier than for classes, since a class can implement
> > multiple
> > interfaces or a parent class and multiple interfaces, but it can still
> > work. This change is perfectly backwards compatible, considering that
> > interfaces aren't even in ES3.
>
> It would involve some algorithm other than the prototype chain walk
> from x, looking for an object === to y.prototype, for (x instanceof
> y), since interfaces do not have prototypes.
>
> But since (x is I) and (x is J) work fine for x = new C where class C
> implements I, J {...}, perhaps for interface types we could just make
> instanceof do what 'is' does. Comments?
>
That works for me.
> > Nevertheless,
> > it would be nice to have a runtime object representing a structural
> > type, that can then be passed to functions as a normal non-type
> > argument, and which can then be used somehow to check the type of an
> > object in a similar matter to |is like|. If structural types could be
> > created or mutated at runtime like constructors, then this would
> > obviously become a necessity.
>
> Agreed. We have not reflected structural types as values because
> there's a syntactic ambiguity for array and record types:
>
> x instanceof {p:int}
> x instanceof [int]
>
> per ES3 11.8.6, which depends on the [[HasInstance]] internal method
> (8.6.2), requires throwing a TypeError here (assume int is bound in
> the scope chain). This is because, for the first example, {p: int} is
> an object initialiser, not a reflection of a record type, and objects
> other than functions do not have a [[HasInstance]] internal method.
>
> But let's say, per ES3 chapter 16, we extend ES4 so that it allows
> what would in ES3 be an error if control flow reached either of the
> above lines for any x (given a binding for int). We've done an
> analoguos extension for the new operator (see the overview, the
> section labeled "Record and array types"). ES4 could say "Aha, that
> looks like an object or array initialiser, but I know it is a
> structural type expression!" and "upgrade" (x instanceof {p:int}) to
> (x is {p:int}).
>
> Is this worth it? It seemed not to anyone working on this in TG1. We
> left instanceof alone, but by giving classes prototype objects and
> making the standard "class constructors" (Object, Date, etc.) be true
> classes, we kept backward compatibility.
>
How about |obj instanceof type {a: int}|?
This makes me wonder: what exactly does |type x| in a value expr resolve
to? If it "resolves to a type", that what does that exactly mean? For
type expr nested within a value expr, that implies to me that it
resolves to a runtime representation of that type. And that implies that
every type must have a runtime representation, including structural
types, e.g. |type {a: int}| would resolve to some runtime representation
of the structural type defined by |{a: int}|.
But from what I've heard and read up to now, |type x| isn't as generic
as I'm implying above, so I'm confused.
In any case, one advantage of having a runtime representation for every
type is that structural types can now be more easily used in a ES3-style
without the usage of type parameters and also allow type bindings to be
computed at runtime, e.g.
function foo(x, t) {
if (!(x instanceof t))
throw some_type_error;
print(x);
}
...
let t;
if (cond) // note how t can only be determined at runtime here
t = type {a: int};
else
t = type like {a: double};
...
foo(obj, t);
> You're right that evolving a language while keeping compatibility
> makes for more combinations that might want to work together, in some
> upgraded sense, than if one "minimizes" and leaves the old form
> (instanceof) alone, but handles the new combinations as well as the
> old ones in the new form (is). This is a tricky issue, which we've
> been keenly aware of, but we don't always attend to perfectly --
> thanks for keeping after us on it.
>
No problem :)
> > |class| and
> > |interface| bind runtime class and interface objects, respectively, to
> > fixed properties.
>
> Without getting into runtime vs. compile-time, the above seems better
> to me if you strike "runtime" from before "class and interface objects".
>
> Another way of looking at types, if you do distinguish compile-time
> and runtime, is that types exist as compile-time values *and* runtime
> values. Usually the latter are just called "values", but I agree with
> Lars, who has argued that values exist at compile time too, and
> "value" is the right word for both, appropriately qualified. Unlike
> some values which can't be known at compile-time, a type is always a
> compile-time value and a runtime value.
>
> If you buy this, then the sentence cited above could say "bind
> compile-time class and interface types, which appear at runtime as
> objects, to fixed properties." Or something like that ;-).
>
> Since ES4 does not require an implementation to support strict mode,
> and since we are trying to avoid requiring analyses that would need a
> full abstract syntax tree or anything more complicated (control flow
> graph, SSA) to be built for whole functions, we intentionally want to
> support the first point of view, that there is no compile- vs.
> runtime distinction.
>
> ES1-3 support this point of view, in the practical sense that its
> chapter 16 talks about SyntaxError being throwable early (but does
> not mandate this, i.e., does not mandate compile-time error
> checking). ES1-3 intentionally allow that implementations may
> interpret something close to source code, modulo transformations of
> for and for-in loops, and a few other places that reorder code. There
> are two passes required to fulfill the obligations of ES3 chapter 10,
> e.g., defining function-valued properties first based on the parsed
> function definitions in a program or function body -- but this is not
> compile-time in the type-checking sense that is optional in ES4.
>
> Obviously we are not done proving that ES4 requires no compile-time
> analyses beyond ES3, but that's our intention. If you see problems in
> the RI or anywhere else, please do feel free to point them out.
>
Alright, that makes sense. I'll stick with the terminology "early type
checking" in the future to avoid all this semantic confusion.
> You're right, and I don't mean to dismiss anything -- more the
> opposite. The "gotchas" in ES3 may become less grabby, or even go
> away, if we make the right additions to ES4.
>
> Exposing underlying "magic" in the built-ins, I think, and letting
> programmers use those powers, is one example. In ES3 there's no way
> to make a constructor function whose prototype property is ReadOnly
> and DontEnum, e.g. Or even simpler: there is no way to turn on the
> DontEnum attribute for a property. ES4 adds optional ways to do these
> things (classes, the new second arg to propertyIsEnumerable). These
> make ES4, by some counts, have fewer gotchas.
>
You're right, ES4 does fix some gotchas in ES3.
> But back to type vs. value expressions, which I agree is a new
> gotcha. But we're not done, and es4-discuss is a huge help in
> finishing well. So let's keep corresponding.
>
> My belief is that merging type and value expressions is right out --
> we've accepted solid proposals for structural types, and they mimic
> the syntax for the related value expressions (functions,
> initialisers). This leaves us with a grammatical divide between types
> and values, but special forms such as annotations only take type
> expressions. So as you point out, the operators such as 'is' and
> 'instanceof' seem to be where worlds collide visibly.
>
> So questions I see include:
>
> * Should we "upgrade" instanceof to work like 'is' when given an
> interface or structural type as its right operand?
>
> * Should we allow value expressions on the right of 'is'? We've
> decided not to already, most recently in September. But it seems to
> me you are asking for this too.
>
> Let me know if I'm missing anything. Thanks,
>
> /be
>
I'm going to try to order my thoughts by listing the possible type
testing functionalities and how they map to operators.
(a) object ISA class or interface (O(1), early test)
(b) object's class is a superset of a structural type (O(1), early test)
(c) object is compatible with a structural type (O(n) where n=# props in
structural type, late test)
(d) object's prototype chain includes the prototype of another object
(O(n) where n=length of chain, late test)
(e) object ISA x, where x is determined at runtime to be a class or
interface (O(1), late test)
(f) object's class is a superset of x, where x is determined at runtime
to be a structural type (O(1), late test)
(g) object is compatible with x, where x is determined at runtime to be
a structural type (same as (c))
|is| does (a), (b)
|is like| does (c)
|instanceof| does (d)
|instanceof| does (e) for classes
nothing does (e) for interfaces
nothing does (f) and (g) since there's no "runtime structural type"
The asymptotic times don't really matter - only whether the test can be
performed early does. If something can be tested early, that implies
that it has to be a type expr. Late testing implies value exprs.
However, although |is| seems to be capable of doing early testing, if no
type exceptions are thrown early for |is|, then it is in effect a purely
runtime check, right?
Furthermore, the syntax of type exprs and value exprs are incompatible
(neither can be subset of each other) because of the following issues
(AFAIK):
1) structural type syntax of type expr collides with object and array
literal syntax of value expr
2) union type syntax collides with comma expr syntax of value expr (e.g.
|(1,2)|)
3) type parameter syntax of type expr collides with type argument syntax
of value expr
I've been scratching my head for a good proposal that coherently
distinguishes |is| and |instanceof| and the associated syntax, but I've
restarted several times already with no solution. Now I've begun to
wonder why the two operators can't be unified in some way, considering
that both are runtime checks. If every type had a runtime representation
then this could work (see example I gave above for structural types).
-Yuh-Ruey Chen
More information about the Es4-discuss
mailing list