need some clarification on compile-time type vs. run-time type
Brendan Eich
brendan at mozilla.org
Sat Nov 10 10:41:28 PST 2007
On Nov 9, 2007, at 11:24 PM, Yuh-Ruey Chen wrote:
>> function MyConstructor(a, b) {
>> return {a: a, b: b} : {a: int, b: string};
>> }
>>
>
> 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.
> Didn't see that on the wiki
http://wiki.ecmascript.org/doku.php?
id=proposals:structural_types_and_typing_of_initializers
> and it's not implemented in the RI yet.
It looks like it almost works, but for an easy-to-fix bug:
>> let q = {p:42} : {p:int}
[stack] [init q()]
**ERROR** EvalError: typecheck failed, val=obj type={p: [ns public
'__ES4__']::int } wanted={p: [ns public '__ES4__']::int } (near <no
filename>:1:1-1.3)
The val and wanted parts of the diagnostic look the same to me.
Should be fixed soon.
>> Above you have made three different things. The instanceof check is
>> not the same as the |is| check. The type paramter example is yet
>> again different -- it's just printing x assuming x is compatible with
>> t -- that is, that there's no type error on attempt to call foo.<T>,
>> e.g. foo.<Date>(new RegExp).
>>
>
> 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 ;-).
> What's the rationale behind not throwing a type error when calling
> foo.<Date>(new RegExp)?
Sorry, I didn't say that, but what I wrote was confusing on second
look. I was simply pointing out that there *will* be a type error,
uncaught, which is not the same as an instanceof or 'is' boolean
test: (new Date instanceof RegExp) => false, no exception thrown.
> 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.
For structural types, we have not tried to upgrade instanceof, mostly
because we didn't want to change instanceof much, but also because
structural types look like value expressions syntactically. More below.
> Allowing |instanceof| to work on classes can trick people into
> thinking that |instanceof| can work on type exprs.
It might, you're right. On the other hand, we can't keep treating
Object or Array as a constructor function, whose name in the global
object can be re-bound. That is not even consistent in ES3. See
http://wiki.ecmascript.org/doku.php?id=clarification:which_prototype
> Yet at the same time,
> since the ES3 builtin constructors are now classes, this feature is
> required for backwards compatibility.
Indeed, and that still seems like the best way forward. But you could
be right that we have not upgraded instanceof enough.
> 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 just leaves the question of structural types, which, although I
> would like |instanceof| to also work on for the sake of completeness,
> doesn't seem worth the effort. As long as structural types are all
> defined at compile-time, |is like| is all that's needed.
Or even just 'is' -- you don't need 'is like' if you want a type
check that demands fixtures matching the structural type's fields.
The benefit of 'is' composed with 'like' is when you want a one-time
check or assertion, and your code does not need to worry about
mutation violating the type a split-second later. From the overview:
function fringe(tree) {
if (tree is like {left:*, right:*}) {
for (let leaf in fringe(tree.left))
yield leaf
for (let leaf in fringe(tree.right))
yield leaf
}
else
yield tree
}
let tree = { left: { left: 37, right: 42 }, right: "foo" }
for ( let x in fringe(tree) )
print(x)
> 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.
Note that making the standard ES3 constructors be classes, we can
explain the otherwise ad-hoc difference in ES1-3 between f.prototype
for a function f (this property is DontDelete) and C.prototype for a
built-in constructor function C (where the property is DontDelete,
ReadOnly, and DontEnum).
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.
> There are type expressions and value expressions. Type expressions are
> adequately defined on the wiki. The identifiers in type expressions
> refer to fixed properties. |class|, |interface|, and |type| statements
> all define types and bind those types to fixed properties.
Right.
> |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.
> A value expression is the typical ES3 expression with
> all the new ES4 syntax extensions, but it can include certain type
> expressions in certain circumstances. Type expressions that refer to a
> class (or parameterized class) resolve to the runtime class object
> (this
> is why |let myint = int| isn't a syntax error). Ditto for interfaces.
> Type exprs are used in |like|, |is|, |to|, |wrap|, and |cast|
> expressions as the second operand (or the only operand in the case of
> unary operators). Type expressions are also used as type
> annotations in
> value expressions. All other cases of type expressions appearing in
> value expressions are syntax errors.
>
> Is that all correct?
The unary 'type' operator is there too (I think -- the wiki page
cited above is old and the RI is not handling this operator correctly
at the moment), but otherwise I think that's correct.
>>> At the very least, the differences and similarities need to be
>>> fully and
>>> carefully documented. ES3 already has plenty of gotchas, and ES4
>>> seems
>>> to be introducing plenty more.
>>
>> It's true that ES4 is introducing optional types. But remember,
>> they're optional. You don't have to use them, but if you choose to,
>> you need to follow the rules about using type definitions or
>> equivalent (class, interface) to make bindings that are fixed
>> typenames.
>>
>
> Offtopic rant: TBH, all this talk of the new features being optional
> detracts from other issues.
Fair enough, but it wasn't "all this talk", it was just one paragraph
in my big fat reply :-). My point was that optionality allows for non-
orthogonality, and backward compatibility may in fact tie our hands
and prevent, e.g., instanceof "upgrades".
> Sure, saying a feature is optional is nice
> for compatibility and all, but I think more effort should be spent
> elaborating on how the new features mesh with the existing features
> in a
> coherent matter. Any backwards-compatible feature is optional. It
> can be
> useless or ugly, but hey it's optional. It reinforces the "everything
> but kitchen sink" feel of ES4, which is not what you should want to
> emphasis. With that said, I'm not saying the type system is
> inelegant -
> in fact, I consider many aspects of it elegant or nifty - but you
> shouldn't dismiss the number of extra gotchas with "hey it's
> optional".
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.
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
More information about the Es4-discuss
mailing list