Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property name collisions. #9

Open
trusktr opened this issue Dec 6, 2019 · 7 comments
Open

Property name collisions. #9

trusktr opened this issue Dec 6, 2019 · 7 comments

Comments

@trusktr
Copy link

trusktr commented Dec 6, 2019

It'd be great if a feature at the language level includes a way to call (or rename) properties or methods that have the same name in different mixins applied to one class, as well as having options for how to handle the diamond problem. Any thoughts on those issues?

@justinfagnani
Copy link
Collaborator

I agree that it'd be great for the language to help with this situation. I think whatever the solution, it should apply equally to classes and mixins.

Symbols are a great primitive to use here, and already partially solve this problem.

Imagine a class that defines symbol-named properties:

const foo = Symbol('A.foo');
const bar = Symbol('A.bar');

class A {
  static foo = foo;
  static bar = foo;

  [foo] = 1;
  [bar]() { console.log(this[foo]); }
}

We can subclass it, safe from naming collisions:

class B extends A {
  [A.bar]() { console.log(this[A.foo], 'from B'); }
}

The computed property name syntax is even pretty nice here - I don't think it needs much improvement. The real issue is that declaring manually namespaced names is cumbersome, so people are less likely to do it.

One option I like to simplify declaring namespaced properties is to use decorators. @littledan has previously proposed something like @namespaced built-in decorator:

class A {
  @namespaced foo = 1;
  @namespaced bar() { console.log(this[A.foo]); }
}

@namespaced would automatically create a static property with the same name as the decorated member and a symbol as a value, then rename the instance member to be named by that symbol.

If we had this mechanism for classes, it would naturally apply to mixins. This would need to be a separate proposal. It's also one reason I really like the built-in decorators proposal, because for static analysis we need there to be a standard common understanding of decorators that can change the shape of classes. (cc @DanielRosenwasser and @rbuckton for that point).

@trusktr
Copy link
Author

trusktr commented Dec 15, 2019

The problem is, we can't expect everyone to use symbols (or @namespaced) for every property of every class (or mixin) that they write.

Also having to use the namespaces for every property access would be less ergonomic, when most of the time it wouldn't be needed. It's only the collision case where it is really needed.

I believe there needs to be a way to look up based on string key names. I wonder how it can be done.

@trusktr
Copy link
Author

trusktr commented Dec 15, 2019

I want to write simple classes like these, which work great standalone with syntax we all are familiar with:

class Size {
  x = 1
  y = 2
  log () { console.log('size: ', this.x, this.y) }
}

let size = new Size(10, 20)
size.log()
class Position {
  x = 3
  y = 4
  log () { console.log('position: ', this.x, this.y) }
}

let position = new Position(30, 40)
position.log()

Now suppose we had a language feature to mix classes together (instead of class-factories):

class Thing extends Position, Size {
  log() {
    // how?
    console.log('thing size:', ???)
    console.log('thing position:', ???)
  }
}

Maybe, by default, classes used in this fashion would be automatically namespaced in their own code, and the subclass could explicitly use the name space.

Without new syntax, it might be like this:

class Thing extends Position, Size {
  log() {
    console.log('thing size:', Object.get(this, Size, 'x'), Object.get(this, Size, 'y'))
    console.log('thing position:', Object.get(this, Position, 'x'), Object.get(this, Position, 'y'))
  }
}

new Thing().log()
// output:
// thing size: 1 2
// thing position: 3 4

This isn't as ergonomic as regular properties, but no new syntax, and only needed in limited cases where collisions happen. Most of the time it isn't needed.

There could be a rule, that by default accessing a key grabs the value from the first class (namespace) in the extends class list. So,

class Thing extends Position, Size {
  log() {
    console.log('thing size:', this.x, this.y)
    console.log('thing position:', this.x, this.y)
  }
}

new Thing().log()
// output:
// thing size: 1 2
// thing position: 1 2

The less ergonomic syntax can be used only when needed, for the occluded properties:

class Thing extends Position, Size {
  log() {
    console.log('thing size:', this.x, this.y)
    console.log('thing position:', Object.get(this, Position, 'x'), Object.get(this, Position, 'y'))
  }
}

new Thing().log()
// output:
// thing size: 1 2
// thing position: 3 4

@trusktr
Copy link
Author

trusktr commented Dec 15, 2019

If we had a feature like the previous comment, with rules around property access priority, then the properties and implied namespaces would be statically analyzable.

@trusktr
Copy link
Author

trusktr commented Dec 15, 2019

Because I know someone is going to ask, here are thoughts on how namespacing might work at a very high level:

Based on how JavaScript works, where all properties are on this (a single instance), and they do not exist in per-instance super objects like they do in languages like Java, then the namespaced properties would need to exist on this, and they'd be created based on any classes in the extends and during construction or inside any method of mixed classes.

If by default mixed class properties are namespaced internally by the engine, and the engine knows which class a method is from, then it can access properties namespaced as needed when it knows there are collisions, and it can know about collisions because it can know (statically) every class that this extends from.

I think this is vague, but with enough ideas to spark some imagination on how it could work.

@trusktr
Copy link
Author

trusktr commented Dec 15, 2019

I have a basic implementation with tests here:

npm i
npm test

The syntax is of course not native, so it is like

import {multiple} from 'lowclass'
class One {...}
class Two {...}
class Three {...}
class Thing extends multiple(One, Two, Three) { ... }

A native-like syntax would be easy to make with Babel.

It's simple at the moment with various edge cases to be solved (some things difficult to achieve with only existing Proxy features).

TODO:

  • implement namespaced access to handle property name collisions
  • individual constructor calls allowing to pass specific args to each constructor

@FrameMuse
Copy link

Could this be a part of the class that receives mixins?

mixin Vector2 {
	x = 0
	y = 0

	divide() {...}
	multiple() {...}
}

class Point with Vector2 {
	constructor() {
		this.x // accesses as if it's defined by the class.
		mixins.Vector2.x // accesses `x` defined by the mixin.
		this[Symbol.mixins].Vector2.x // Alternative without new syntax.
	}
}

But honestly not sure if this useful, I believe the best way to handle this is to just throw an error if there are conflicting names. Do you have any world-examples where it could be useful - I'm curious.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants