Ren-C has a more streamlined version of how R3-Alpha implemented simple OBJECT!s, but it's really mostly the same (though MODULE! has changed significantly)
An OBJECT! is just two parallel lists, which I have called the "keylist" and the "varlist".
So if you say something like:
obj: make object! [
x: 1 + 2
y: 10 + 20
]
You will get:
keylist: {symbol(x) symbol(y)}
varlist: [*V0* 3 30]
The first slot in a varlist is used for some tracking information. So:
-
keylist[0]
is the key forvarlist[1]
-
keylist[1]
is the key forvarlist[2]
You Get A New Keylist With Every MAKE OBJECT!
Nothing in the system goes around looking for common patterns in your object creation to notice that you've made several objects with the same keys.
collect [
count-up i 1000 [
keep make object! [x: i * 10, y: i * 20]
]
]
You just made 1000 objects, and all of them have their own copy of the keylist {symbol(X) symbol(Y)}
. Ren-C made this overhead cost less than 1/4 as much as R3-Alpha, but it's still kind of lame.
The only way you avoid making a new keylist is if you do object inheritance.
point!: make object! [x: y: null]
collect [
count-up i 1000 [
keep make point! [x: i * 10, y: i * 20]
]
]
This time, there's 1000 objects all sharing a single keylist.
If you expand keys at all, that will result in a new keylist...
You spoil the optimization if you put anything additional in your derived object:
point!: make object! [x: y: null]
collect [
count-up i 1000 [
keep make point! [x: i * 10, y: i * 20, z: i * 30]
]
]
There's no inheritance mechanism that makes use of the common sublist. So this puts you at 1001 keylists, because your keylist for the original point! never gets used.
Object Expansion via APPEND disconnects shared keylists
R3-Alpha allowed you to add fields to an object. If you did so, you would lose any sharing that it had taken advantage of before.
p: make point! [x: 10 y: 20] ; reuses point!'s keylist
append p [z: 30] ; oop, not anymore...gets its own keylist.
Comparisons Are Difficult
Because there's no global mechanism of canonization of keylists, you get entirely different-looking objects by creating the fields in different orders.
obj1: make object! [x: 10 y: 20]
obj2: make object! [y: 20 x: 10]
These objects have been considered to be not equal historically. Because comparisons are done by walking the fields in order. So obj1 <> obj2 in this case.
However, if you create an object via inheritance so it shares a keylist, that will standardize the order of the fields:
point1: make point! [x: 10 y: 20]
point2: make point! [y: 20 x: 10]
Here we will have point1 = point2, since their shared keylist forces the order of x and y to whatever it was in POINT!.
There Are Fancier Ways Of Dealing With This
If you're willing to say that the order of keys in objects shouldn't matter... then you can rethink the data structures to exploit commonalities in the patterns of keys that are created.
The V8 JavaScript engine approaches this with Hidden Classes.
But there's really always some other way of approaching the problem. The way modules work in "Sea of Words" is an example of a structure that seems to work reasonably well for modules--but wouldn't work as well for lots of little objects.
Today's FRAME! Depends On This Non-Fancy Way
Right now, when a native runs it does so with a concept of the order of the arguments and refinements that gets baked into the C code directly. IF knows that the condition is argument 1 and that the branch is argument 2, and it looks directly in slots 1 and 2 of the varlist of the frame to find those variables.
This is pretty foundational to the idea of the language, and is part of what gives it an appealing "simple-ness".
Ren-C has come along and permitted higher level mechanisms like specialization and adaptation, but everything is always getting resolved in a way that each step in a function's composition works on putting information into the exact numbered slot that the lower levels expect it to be in.
Binding Has Depended On This Non-Fancy Way
A premise in Rebol has been that you can make a connection between a variable and an object that has a key with the name of that variable, and once that connection is made it will last. This rule is why there's been dodginess about deleting keys in objects or rearranging them...and why R3-Alpha permits adding new variables but not removing any.
obj: make object! [x: 10 y: 20]
code: [x + y]
bind code obj
If you write something like the above, you are annotating the X inside of CODE with (obj field #1), and the Y inside of CODE with (obj field #2). So nothing can happen with obj that can break that.
This isn't strictly necessary. It could have annotated X and Y with just (obj) and then gone searching each time it wanted to find it. This would permit arbitrary rearrangement of OBJ, inserting and removing keys. It could even remove X or Y and then tell you it couldn't find them anymore.
There are compromises as well. The binding could be treated as a potentially fallible cache...it could look in that slot position (if it's less than the total keylist size) and see if the key matched. If not, it could fall back on searching and then update with the slot where it saw the field.
(Of course this means you have to look at the keylist instead of just jumping to where you want to be in the varlist, and locality is such that they may not be close together; so having to look at the keylist at all will bring you a slowdown.)
But What Is The Goal, Here?
I've mentioned how the FRAME! design pretty much seems to go along well with the naive ordering of object fields.
I guess this is where your intuition comes in as to what represents "sticking to the rules of the game". And I think that hardcoding of positions into the executable of where to find the argument cells for natives is one of the rules.
This suggests that all functions hardcode the positions of their arguments--even usermode functions. I'm okay with this.
So then we get to considering the question about OBJECT!.
-
A lot of languages force you to predefine the structure of an object before creating instances. And defining that structure is a good place to define its interfaces. If Rebol wants to go in a more formal direction (resembling a Rust/Haskell/C++) then you might suggest you always make a base structure...and you can only have the fields named in it.
-
Other languages (like JavaScript) are more freeform, and as mentioned can look for the relationships after-the-fact. Order of fields does not matter.
It's clear that Rebol's userbase so far are people who would favor better implementation of the JavaScript model over going to more strictness. I think there'd be a pretty good reception of a model where you could create objects with {...} and where fields could be added or removed as people saw fit. If behind-the-scenes the system was optimizing access to those objects, that would presumably be preferable to this idea that you had to be responsible for declaring prototypes to get efficiencies (that would instantly disappear if you added another field).
But the mechanics definitely get more complicated. :-/