typed-objects-explainer icon indicating copy to clipboard operation
typed-objects-explainer copied to clipboard

new StructType() vs makeStruct()

Open phpnode opened this issue 9 years ago • 12 comments

I'm implementing a typed objects library (not yet released) and trying to follow the proposals here as closely as possible so that one day it can be made compatible with the final spec.

In my implementation, I've gone with

const Pixel = makeStruct({r: uint8, g: uint8, b: uint8, a: uint8});
const pixel = new Pixel();

rather than

const Pixel = new StructType({r: uint8, g: uint8, b: uint8, a: uint8});
Pixel instanceof StructType; // presumably true, shimming requires support for Symbol.hasInstance
const pixel = new Pixel();
pixel instanceof StructType; // presumably false

because the former seems quite a lot more natural and easier to support in older environments (and probably faster too). What's the rationale behind using new StructType() other than the fact that the user is literally defining a new struct type?

phpnode avatar Dec 28 '15 23:12 phpnode

I'm implementing a typed objects library (not yet released) and trying to follow the proposals here as closely as possible so that one day it can be made compatible with the final spec.

Excellent!

because the former seems quite a lot more natural and easier to support in older environments (and probably faster too). What's the rationale behind using new StructType() other than the fact that the user is literally defining a new struct type?

Hmm, yes. I've been thinking about the right subclassing relationships for all this, and there are good arguments for making new Pixel() instanceof StructType hold. In fact, the prototype relationships described in my current draft would mean that it would hold. At the very least, that section is inconsistent with creating struct type definitions via new StructType.

makeStruct isn't entirely right either, though: you're creating a struct type not a struct, after all. Perhaps StructType.create or StructType.new would make sense.

tschneidereit avatar Jan 03 '16 16:01 tschneidereit

you're creating a struct type not a struct, after all.

True, maybe makeStructType is better, but it's not ideal. If the consensus is that new Pixel() instanceof StructType is true, then the language already has a declarative way of expressing that:

class Pixel extends StructType {}

But obviously this reraises the question of how to define field types and defaults.

phpnode avatar Jan 03 '16 17:01 phpnode

If the consensus is that new Pixel() instanceof StructType is true, then the language already has a declarative way of expressing that: class Pixel extends StructType {}

Sure, as far as the inheritance relationship is concerned, that works fine. As does

function Pixel() {};
Pixel.prototype = Obiect.create(StructType.prototype);

Note, though, that neither is sufficient to fully define a struct type: they both create constructors whose instances would be plain old JS objects, without the special internal layout required for struct type objects.

You can find a very preliminary draft of a class syntax explainer here. The class syntax is explicitly not part of the core proposal because it's not important for the specification of typed objects' semantics. (I.e., you can have the proposed semantics without the class syntax, but you need to fully describe the semantics for the class syntax to even matter.)

tschneidereit avatar Jan 03 '16 17:01 tschneidereit

Note, though, that neither is sufficient to fully define a struct type: they both create constructors whose instances would be plain old JS objects, without the special internal layout required for struct type objects.

Yes, true and the other problem is that the only obvious way to declare fields is with decorators (not yet accepted into the spec) or hypothetical type-annotated class properties (not even proposed afaik), and I think both approaches suffer from the "finalization problem" - they'd require that the layout for a struct can be modified after it is first declared, and obviously that leads to the problem of when finalization should occur and that's a horrible bag of worms.

Slightly related to this, I ran into an issue where I wanted to represent a simple recursive tree structure, but the current proposal affords no way for a type to refer to itself, for example the following doesn't work, because TreeNode is obviously not defined when I need it:

const TreeNode = makeStruct({
  value: uint8,
  left: TreeNode, // nope
  right: TreeNode // nope
});

The solution I went with is to allow the first argument to makeStruct to be a function, which takes the partial representation of the type as first argument and returns a configuration object:

const TreeNode = makeStruct(TreeNode => ({
  value: uint8,
  left: TreeNode, // nullable pointer to a TreeNode, cannot be embedded
  right: TreeNode // another
}));

This doesn't really feel great, it's possibly a bit too magic, I wonder if you had already discussed this kind of stuff and had any other suggestions?

phpnode avatar Jan 03 '16 20:01 phpnode

Hmm, I still feel that, of the available options, new StructType is the best choice. After all, a new object is always returned, and it is indeed a new struct type. As for the prototype hierarchy, I do not think that object instanceof StructType should necessarily be true. Perhaps I did not read your edits closely enough (or did not setup the original correctly), but I believe the intention was that StructType.prototype was the prototype for the user-defined type descriptors and StructType.prototype.prototype is the "prototype^2" for the instances of those type descriptors. IOW:

var PointType = new StructType(...);
PointerType instanceof StructType; // true
PointType.__proto__ === StructType.prototype; // true

var point = new PointType();
point instanceof PointType; // true
point instanceof StructType; // false
point instanceof StructType.prototype; // true
point.__proto__ === PointType.prototype; // true
point.__proto__.__proto__
    === PointType.prototype.__proto__
    === StructType.prototype.prototype; // true

Under this system:

  • I can attach methods to all struct types by putting them on StructType.prototype
  • I can attach methods to PointType by editing, well, PointType
  • I can attach methods to all points by editing PointType.prototype
  • I can attach methods to all typed objects by editing StructType.prototype.prototype

nikomatsakis avatar Jan 04 '16 15:01 nikomatsakis

This all feels a bit strange compared to how classes in JS normally work. I can't think of another example where new Something() would return a new class which extends Something rather than an instance. Also it introduces some complexity in the constructor:

class Point extends StructType {
  constructor (...args) {
    // here I must call super(), but not with args because that will result 
    // in very strange behaviour.
    // Also, the StructType constructor needs to differentiate between
    // super() calls and new StructType() calls. 
  }
}

The issue of whether all structs should inherit from StructType is a different concern and could still be possible, maybe StructType.make(fields, options) is another alternative.

phpnode avatar Jan 04 '16 15:01 phpnode

This all feels a bit strange compared to how classes in JS normally work. I can't think of another example where new Something() would return a new class which extends Something rather than an instance.

That's part of my concerns, too.

I'm also not sure we need to make StructType a sort of meta class instead of a base class for all, well, struct types. It's still possible to attach methods in all the ways described above:

  • I can attach methods to all struct types by putting them on StructType.__proto__
  • I can attach methods to PointType by editing, well, PointType
  • I can attach methods to all points by editing PointType.prototype
  • I can attach methods to all typed objects by editing StructType.prototype

tschneidereit avatar Jan 04 '16 16:01 tschneidereit

This all feels a bit strange compared to how classes in JS normally work.

Maybe, I'm not sure what is the most analogous situation in JS.

I can't think of another example where new Something() would return a new class which extends Something rather than an instance.

But new StructType IS returning an instance -- an instance of StructType (which is a type descriptor). Instantiating that type descriptor yields you a particular object.

For example, instanceof works just like you expect:

const Point = new StructType(...);
Point instanceof StructType; // just like any other class!
Point.__proto__ === StructType.prototype; // just like any other class!

Basically, the analogy to Smalltalk's metaclasses and classes seems correct, and is certainly how I always thought of it. That is, StructType is a metaclass, so instantiating it yields you a class. But, since a class is an object, a metaclass is also a kind of class. It's all a beautiful recursive cycle. :)

That said, I'm not married to the current setup, but it seems very self-consistent. In contrast, if we try to make StructType into a supertype for typed objects (as opposed to type descriptors), then we need to add something like StructType.create, which seems like brand new precedent: that is, the fact that new StructType vs StructType.create(...) are so different seems mildly surprising to me.

I guess an important question is what are other precedents for factories in JS that yield types (and not object instances)?

UPDATE: changed name from PointType to Point, since that was not following my "Type suffix means meta-class" convention. :)

nikomatsakis avatar Jan 04 '16 18:01 nikomatsakis

(I haven't thought hard about the polyfill question, but I thought @dherman or perhaps others had some polyfills for the current setup. Not sure what they did in this situation.)

nikomatsakis avatar Jan 04 '16 18:01 nikomatsakis

@tschneidereit

Some further thoughts. First, it seems to me that if StructType is not a meta-class, then it should be called just Struct (just like Array and String are not called ArrayType and StringType). So just for shorthand I'll call things Struct when I'm using your approach. That way new Point() instanceof Struct is true, which makes sense.

OK, so, that out of the way, I'm confused by what you wrote about adding methods:

I can attach methods to all struct types by putting them on StructType.__proto__

It seems like under your setup you would add them just to Struct (which you called StructType, no? I don't really know what I expect Struct.__proto__ to be, presumably whatever Array.__proto__ is (since Struct is a subclass of Object just like Array).

I can attach methods to PointType by editing, well, PointType

Yes.

I can attach methods to all points by editing PointType.prototype

Yes.

I can attach methods to all typed objects by editing StructType.prototype

I would call it Struct.prototype, but yes.

nikomatsakis avatar Jan 04 '16 18:01 nikomatsakis

Now, if we DO introduce a name like Struct, then of course we could still keep the metaclass StructType, and just have Struct.prototype === StructType.prototype.prototype :)

nikomatsakis avatar Jan 04 '16 18:01 nikomatsakis

Update: Array.__proto__ === Function.prototype, so I would expect Struct.__proto__ to be the same. This also ensures that Point instanceof Function, right? I need to make a diagram!

nikomatsakis avatar Jan 04 '16 18:01 nikomatsakis