typed-objects-explainer
typed-objects-explainer copied to clipboard
new StructType() vs makeStruct()
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?
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.
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.
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.)
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?
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
PointTypeby 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
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.
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
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. :)
(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.)
@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.
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 :)
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!