Skip to content

What does super.foo resolve to in protocol methods, if anything? #88

@LeaVerou

Description

@LeaVerou

This came up while discussing #76.

If protocol methods take precedence over inherited methods, a very common pattern is to extend base methods, same as the class itself would. In fact, @justinfagnani brought this up in #33 (comment) as primary motivation for a separate mixins syntax.

One option for super.foo is to resolve relative to the parent protocol, if we end up having extends and not just implements. But it's a bit suboptimal. First, what does that even mean, since the parent protocol has different symbols? Second, if protocol methods can never extend superclass methods on the objects they are implemented on, that's a bit limiting. If anything, perhaps that points towards only having implements on protocols and not also extends.

While the [[HomeObject]] of these functions cannot vary, having super.foo resolve to something close to what it would resolve on a regular method could be far more useful useful.

E.g. this is a bit niche but just to throw a concrete example in, suppose you have a protocol to keep track of custom element shadow roots and prevent errors from multiple calls. A protocol for that could look like this (assuming a resolution to #76 where protocols win over inherited methods):

const shadowRoot = Symbol("shadowRoot");
const allRoots = new Set();

export protocol TrackedShadows {
	["attachShadow"](...args) {
		this[shadowRoot] ??= super.attachShadow(...args);
		allRoots.add(this[shadowRoot]);
		return this[shadowRoot];
	}
}

Then implemented like:

class MyElement extends HTMLElement implements TrackedShadows {
	// elided
}

@michaelficarra said we could have an exception in GetSuperBase() where for protocol methods we use this instead of [[HomeObject]]. Then, super.foo resolves as Object.getPrototypeOf(this).foo.
However, note that this would cause an infinite loop in this case: in myElement (instance of MyElement) super.attachShadow() would resolve to Object.getPrototypeOf(myElement).attachShadow, which is MyElement.prototype.attachShadow() …which is the protocol method! What we want instead is Object.getPrototypeOf(Object.getPrototypeOf(myElement)).attachShadow. But that doesn't generalize: e.g. on static methods you only need one Object.getPrototypeOf() call.

We could special-case to constructors and their instances, but I think the more agnostic they can be to the object they are implemented on, the better.

A better algorithm might be to create the exception in the semantics of computing SuperProperty.

Suppose fn is the function we have from the protocol. I wonder if we could do something like this instead:

let superBase = Object.getPrototypeOf(this);
while (superBase[propertyKey] === fn) {
  superBase = Object.getPrototypeOf(superBase);
}
return superBase[propertyKey];

If the function is called with some random this, then no problem, it just never loops.
(This is obviously a simplification, since it needs to also work for accessors)

That is completely agnostic to constructors, yet resolves as expected on both instances and static methods. It even avoids infinite loops when the parent class has implemented the same protocol independently.

It does feel a little weird, e.g. one could argue that if the prototype has the same function installed on that property then it should self-recurse at that point, but I'd argue that even in that case, that 99.9% self-recursion is a bug so if we can prevent it, that's a win.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions