Close review of Language Overview whitepaper

Brendan Eich brendan at mozilla.org
Wed Nov 14 23:56:29 PST 2007


On Nov 14, 2007, at 9:51 PM, Kris Zyp wrote:

>>> JSON: Sounds good.
>> This proposal is withdrawn and another API is being considered for
>> reinclusion later. See http:;//json.org/json2.js.
> toJSONString and parseJSON are going away? I was actually wanting  
> to write
> and suggest the removal of these, with Douglas's recent change in  
> his JSON
> API. I am glad to see these will be going away. Will ES4 include  
> the other
> API (JSON.parseJSON / JSON.stringify)?

(It's JSON.parse, not JSON.parseJSON.)

I thought I answered that above, sorry if I was unclear. We like  
Doug's new API and hope to include it in due course into ES4. JSON  
was already an accepted proposal, so it has a foot in the door.  
Various interested parties favored something like the json2.js API  
already, and I think everyone will rally round it and beat on it, to  
make sure it has the right usability and knobs. I'm hopeful.

>> You miss the main difference: structural types are not equated by   
>> name, so
>> they avoid the inheritance trap of classes, which consists
> I would love to understand the purpose of structural types better.  
> I don't
> understand how base class evolution is constrained in ways that  
> super record
> types aren't.

You can have an object {p: 42, q: "hi"} match an annotation of the  
form 'like {p:int, q:string}', but there is no class with which that  
object is compatible in any well-defined sense except for Object itself.

While you can make a subclass of Object, say class C {p:int,  
q:string}, be a structural subtype of a record type, say {p:int} or  
{q:string} or {p:int, q:string}, you cannot do the reverse. {p:int,  
q:string} is not a subtype of C.

Evolution is constrained when you want to inherit from two classes,  
which ES4 does not allow. Assume no name conflicts, since those can  
arise with nominal and structural types, and namespaces stand ready  
to help.

The problem in general is that Bob's classes and Alice's classes were  
written without anticipating Carol's combination of the two, but  
Carol cannot use MI. Nor can she provide objects that match  
structural types. She has to inherit from both Bob's and Alice's  
classes.

This inevitably means duplication, peered objects delegating to each  
other, even if Bob and Alice used interfaces well (interfaces help,  
but bring in other problems and can't be used everywhere).

This also means conflicts are hard to resolve without writing more  
(and more verbose) nominally typed code, instead of writing some  
untyped code that satisfies like-types or that can be wrapped with  
tolerable performance, or some structurally typed code (see below for  
an iterator example).

> I also don't understand how the goal of applying types to existing ES3
> objects can not be achieved with wrap operator and nominal types. I  
> know
> there something I am missing here.

You do not want to let any old object masquerade as a nominal type  
(class, in this case). Imagine if you had class C { var p:int,  
q:string ; final function must_not_override() {...} }. Would {p:42,  
q:"hi", must_not_override: function() "rm -rf /"} be safe to pass off  
as 'like C'? How about as 'wrap C'?

Remember, the users of C as a type annotation are not interested in  
the details of C's implementation (even if final; or if not final, of  
a subtype of C coming in as-a C). They care about behavior, in  
general. But at the limit, and in essence (i.e., what the type  
means), they really want a type *named* C, and nothing but that type  
(or of course a subtype, if C is not final).

Ok, ignore final classes and methods, say you are willing to special  
case (I'm kidding, but play along ;-). If you let untyped objects  
wrap into nominal types, you've killed another important use-case.  
Say you want to return instances of a type that no one can construct  
except your code. You don't want anyone wrapping some alien object.  
You really need your own instances and only those instances. Think of  
those capability objects created by private constructors that David  
Teller was asking about the other week (thread head).

There are other use-cases that want nominal types to have this  
"branded type" integrity property, e.g. hybrid information flow,  
which must see all the subtypes in the system in order to analyze all  
effects along implicit flows.

But just for the sake of argument, who cares? Let's say you are hell- 
bent on minimizing even if it undermines your system's security (not  
that this has ever happened before :-P). If you thus reduce the  
integrity of classes until they are named record types, and subtype  
judgments are record-width subtype judgments decoupled from the  
class's extends clause, then you've reinvented structural types. But  
look at the costs.

While like and wrap can be handy for dealing with untyped objects,  
and efficient when the actual value passing through the annotation is  
well-typed, if you want to avoid the potential costs of like (a deep  
shape-test) and wrap (a deep read and write barrier), you really do  
want to allow a type with C's structure, not C's name -- you want  
structural types for the actual arguments flowing in, not like or  
wrap types.

The IteratorType is an example. You don't have to extend a built-in  
class, or implement a built-in interface, to make an iterator. All  
you need is a next method of the right type:

type IteratorType.<T> = {
     next: function (): T
};

You can implement iterators using object initialisers. From the RI's  
builtins/Map.es (with a bug-fix not yet in the monotone source):

         helper function iterate.<T>(f: function(*,*,*):*) {
             let a = [] : [T];
             informative::allElements( tbl, limit, function (k,v) { f 
(a,k,v) } );
             let i = 0;
             return {
                 next: function () : T {
                     if (i === a.length)
                         throw iterator::StopIteration;
                     return a[i++];
                 }
             } : IteratorType.<T>;
         }

You don't need classes, nor should you. You can retrofit and migrate,  
starting with wrap (or like if in the same trust domain), and moving  
to annotate object initialisers over time.

To restate the problems above as tersely as possible:

1. Class integrity means that no one can forge an instance using an  
untyped object, even with wrap.
2. You shouldn't have to use wrap, it costs enough that it must be  
explicit, and not mandatory for all cases where untyped meets typed.
3. Other cases where untyped meets typed can use like.
4. Structurally typing untyped code is lightweight compared to  
subclassing and implementing nominal types.

Modula 3 had branding for making nominal types from structural types,  
but going the other way, "unbranding" a nominal type to get a  
structural type, has no precedent I know of, probably because it  
undermines the integrity property that motivates branding or nominal  
typing in the first place. A class C can mean many things, depending  
on its members, finality, etc. But its meaning is bound to its name,  
not its structure.

Does this clear things up?

/be
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://mail.mozilla.org/pipermail/es-discuss/attachments/20071114/cb56b1f4/attachment-0002.html 


More information about the Es4-discuss mailing list