Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Reflection from superclasses #36

Closed
mbrowne opened this issue Aug 17, 2017 · 7 comments
Closed

Reflection from superclasses #36

mbrowne opened this issue Aug 17, 2017 · 7 comments

Comments

@mbrowne
Copy link

mbrowne commented Aug 17, 2017

Would it be possible for this proposal to include support for reflection over instance properties from a constructor in a superclass? This would be very useful to support something like named constructor parameters, as described in #33. Here's the example again (somewhat modified for clarity for this new thread):

class Entity {
  constructor(attributes) {
  
    //how can we obtain a list of instance property names defined on the child class?
    const propertyKeys = ???
    
    for (let key in propertyKeys) {
      if (key in this) {
        this[key] = attributes[key]
      }
    }
  }
}

class Cat extends Entity {
  name = null
}

const garfield = new Cat({
  name: 'Garfield',
  someExtraProperty: 'foo'  // this will be ignored
})

garfield.name               // 'Garfield'
garfield.someExtraProperty  // undefined

My current understanding of the class fields proposal is that the definition of Cat here would be equivalent to:

class Cat extends Entity {
  constructor(attributes) {
    super(attributes)
    this.name = null
  }
}

Assuming that's the case, then assigning property values in the Entity constructor wouldn't work, since the property isn't defined until after the call to super(). My initial thought was that putting the properties on the prototype in addition to the instance would solve this, but I understand now that this causes too many other issues. But I think there should still be a way to accomplish something like the above, as can be done in other languages like PHP and Java (and probably others).

For this particular example, decorators might provide an alternative solution, since decorators are evaluated early on, e.g.:

class Cat extends Entity {
  @attribute
  name
  
  @attribute
  curious = true
  
  ...
}

(Note: I'm making several assumptions here about the interaction between classes and decorators that I don't think are finalized yet since decorators are still in stage 2.)

Still, I imagine there are other situations where it would be useful to use Object.getOwnPropertyNames or Reflect.ownKeys from a superclass and have it include the names of instance properties defined in a child class.

But Object.getOwnPropertyNames should be semantically correct, and not return properties that don't exist yet...so I suppose what I'm really proposing is that class fields should be evaluated like this:

class Cat extends Entity {
  constructor(attributes) {
    this.name = null
    super(attributes)
  }
}

...which of course wouldn't be valid JS if actually written that way (since you can't reference this before calling super()), but would it be feasible for class fields to work this way behind the scenes? If not, maybe there could be a new reflection method that would mean something like, "tell me the names of properties declared in my class or any of my subclasses, regardless of whether or not they've been initialized yet".

Sorry for the somewhat meandering description; I was trying to cover various considerations and I hope it wasn't confusing.

@ljharb
Copy link
Member

ljharb commented Aug 17, 2017

I think the only way this could be done is with a new feature (ie, a new proposal separate from this one) for a class "finisher" function, that you could define on Entity.

@mbrowne
Copy link
Author

mbrowne commented Aug 17, 2017

That could work if each class (and of particular interest here, each child/parent class) had its own "finisher" function. That way the parent class could do some initialization based on reflection before the rest of the child constructor (the code after super()) executes.

If someone remembers off the top of their head, can you point me to where in previous discussions the timing of field definition/initialization was discussed?

I noticed that there's already a concept of "finishers" in the decorators proposal, which I need to look into in more detail...if that could be expanded to the class level, maybe my goal could be accomplished with syntax as simple as:

@Entity
class Cat {
  name
  curious = true
}

(I realize this isn't the right place for extended discussion about decorators, so I'm happy to move the decorator discussion to that repo instead if more detailed discussion is warranted.)

@littledan
Copy link
Member

I don't quite understand why you want to do this by subclassing. If you'd be OK calling out to a function to do the assignments, it would be as easy as:

function assign(target, attributes) {
  for (let key of Object.keys(attributes)) {
    if (Object.hasOwnProperty(target, key)) {
      target[key] = attributes[key];
    }
  }
}

class Cat {
  name;
  curious = true;
  constructor(attributes) {
    assign(this, attributes);
  }
}

What's the motivation for having this logic in the superclass?

@mbrowne
Copy link
Author

mbrowne commented Aug 17, 2017

There are two main things I'm thinking about here:

  1. How to most simply and concisely implement something approximating named parameters for constructor functions
  2. How the reflection API should ideally work

#1 was my original impetus for creating an issue in this repo, but #2 might actually be more important, since there are several alternatives for implementing #2 (including a simple helper function as you just pointed out).

One of the purposes of reflection is to be able to access compile-time information at run-time (or at least, this is common in other languages...in some languages you can even access the comments above a class or function via reflection). It's difficult to anticipate all the useful ways that reflection could be used, so I would generally prefer access to more information rather than less. As soon as a class declaration has been interpreted, it would be logical to be able to access metadata about that class, including its instance field declarations, via reflection. With objects (as opposed to classes) it's trickier, since the properties don't exist on the object until they've been assigned to this, but if you know which class or subclass's constructor was called, then it should still be possible to look up the field names.

This is a philosophical argument of course, and off the top of my head I can't think of any cases other than the one I presented above where being able to reflect on subclass fields from a parent constructor would be useful. So if there are technical arguments against supporting this, then I suppose the technical arguments should trump the philosophical argument.

Regarding #1, yes, a helper function would work and is already very concise. I was envisioning that the Entity class would be the base class for all domain classes, and might have other common methods/properties as well...perhaps something ORM-related like an entity state (added, unchanged, modified, etc.) If all base entity behaviors could be achieved without a base class, that might be even better - IMO good ORM libraries strive to keep a simple POJO (plain old JS object) model as much as possible. So if all the necessary functionality could be added to the object via a decorator, I would consider this to be ideal:

@entity
class Cat {
    ...
}

Or if a subclass made more sense for some reason, then as an alternative to a standalone utility method, this could also work:

class Entity {
    setAttributes(attributes) {...}
}

class Cat extends Entity {
  ...
  constructor(attributes) {
    this.setAttributes(this, attributes);
  }
}

It's sounding more and more like this use case shouldn't, in and of itself, motivate any change in the current proposal for class fields. It does seem relevant to this proposal and worth discussing, and it's an example of using reflection from a parent constructor - those are the reasons I brought it up.

It's of course a common pattern for a constructor function to accept an object containing the initial state...models in Backbone, Ember, Breeze.js etc. support support this in various ways. I just found a library called ObjectModel, which comes the closest to the particular usage I had in mind with my example:
http://objectmodel.js.org/#doc-object-model

It also has an interesting approach for use with ES6 classes:
http://objectmodel.js.org/#doc-es6-classes

My main goals are simplicity and avoiding boilerplate code in cases where it would be very common.

@littledan
Copy link
Member

OK, it sounds like, between decorators, expressing this functionally, and possibly inheriting from a class whose constructor does all the work (as in that library--then, you would just not use the class fields feature), this problem is solved. If you want to follow up in the decorators repository, that could be good, but I don't really see any missing features there.

@mbrowne
Copy link
Author

mbrowne commented Aug 17, 2017

I agree; I consider that this has been considered ;) and the current implementation options are sufficient for my example. I will explore the latest version of the decorators proposal in more detail and follow up there if warranted.

I still think #2 above (how the reflection API should ideally work) is worth further consideration. But I suppose someone would need to present a compelling use case before it would be worth spending time on. So I'm closing this issue and can reopen it if anyone presents such a use case in the future. Thanks all for following my detailed explanations.

@littledan
Copy link
Member

For context, @wycats previously proposed a full reflection API for fields, but I objected to it because such APIs might give too much flexibility to monkey-patch existing things and give a high implementation burden.

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

No branches or pull requests

3 participants