proposal-refs icon indicating copy to clipboard operation
proposal-refs copied to clipboard

ref properties - Simpler circular objects

Open Jamesernator opened this issue 3 years ago • 0 comments

Currently in order to construct a circular object one needs to do things in two phases:

  • construct a partial object
  • attach circular references to the object

However this pattern can be both quite annoying to use, and fails to integrate well with type systems.

For example consider the following example that tries to define schema (of code that doesn't work):

const Person = {
    name: "string",
    bestFriend: Person,
};

Currently this fails because bestFriend: Person tries to access the variable before it is accessible.

While the fix isn't overly difficult, it also has poor integration with type systems i.e.:

const Person = {
   name: "string",
   // How to declare type of bestFriend here
} as const;

// Oops can't declare the property because Person doesn't have such a field
Person.bestFriend = Person;

I would like to propose that in addition to ref parameters, we also have ref properties on objects. These would be rather similar to ref params in that they would defer evaluation until access time rather than write time.

This is probably best shown with an example:

const Person = {
    name: "string",
    // Look ma, typing just works
    ref bestFriend: ref Person,
} as const;

Now in order for ref propertyName: to associate a ref, there are a couple options, either we could have that ref propertyName is roughly just shorthand for:

const __PersonRef = ref Person;

const Person = {
    name: "string",
    get bestFriend() {
        return __PersonRef.value;
    },
    set bestFriend(bestFriend) {
        // For const variables this would 
        __PersonRef.value = bestFriend;
    }
} as const;

Or we could possibly define that ref propertyName defines exotic behaviour such that Object.getPropertyDescriptor(Person, "bestFriend") would return it as a "normal" property (that just happens to have a hidden backing store).

A wider example of a use

While the above might feel like a somewhat of a toy example, in practice this sort've thing can occur in more common systems. One particular example would be the webassembly gc proposal.

Currently it is not specified how these types will exposed as a JS API, but there is an identical problem that will occur, GC types are by design allowed to be circular.

i.e. Consider a factory that makes a linked list "struct" type:

function LinkedList(valueType: WebAssembly.Type) {
    const LinkedListType = new WebAssembly.StructType({
        value: valueType,
        next: {
            nullable: true,
            // OOPS circular reference
            type: LinkedListType,
        },
    });
    return LinkedListType;
}

Notice this suffers exactly the same problem as the example Person example above.

Again a solution would just be to allow ref properties i.e.:

function LinkedList(valueType: WebAssembly.Type) {
    const LinkedListType = new WebAssembly.StructType({
        value: valueType,
        next: {
            nullable: true,
            // All good now
            ref type: ref LinkedListType,
        },
    });
    return LinkedListType;
}

Now there is one particular difference with this example, and that is that such declarations would almost certainly only want constant-references (because this describes a type in memory, it should not be allowed to magically change). With such references, we could easily write it as such:

function LinkedList(valueType: WebAssembly.Type) {
    const LinkedListType = new WebAssembly.StructType({
        value: valueType,
        next: {
            nullable: true,
            // All good now
            const ref type: ref LinkedListType,
        },
    });
    return LinkedListType;
}

Class Integration

Now the above idea could easily be integrated into classes as well, however they have slightly different issues i.e.:

// Note that SINGLE recursive is already perfectly fine in a class
class Person {
    static name = "string";
    // This works because the Person declaration is available in
    // a partially constructed form 
    static bestFriend = Person;
}

// However mutually recursive classes explode:

class A {
     // B is not defined
     static prop = B;
}

class B {
    static prop = A;
}

We could solve these problems in the same way as for object literals:

class A {
    static ref prop = ref B;
}

class B {
    static ref prop = ref A;
}


A.prop === B; // Hooray works

Again, a decision would need to be made about exposing these as getter/setter pairs or exotic [[Get]] behaviour. Public instance properties would be particularly noticable as we'd need to decide if the following creates OWN or PROTOTYPE properties:

class Example {
    ref field;
    
    constructor() {
    
    }
}

// Proto properties, similar to "accessor" in decorator proposal?
Object.getOwnPropertyDescriptor(Example.prototype, "field");

// Or own properties?
Object.getOwnPropertyDescriptor(new Example(), "field");

// In either case, exotic [[Get]] or are these real getter/setter pairs?

Jamesernator avatar Mar 17 '22 23:03 Jamesernator