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

Public prototype accessor does not support initializer #153

Closed
mweststrate opened this issue Sep 24, 2018 · 5 comments
Closed

Public prototype accessor does not support initializer #153

mweststrate opened this issue Sep 24, 2018 · 5 comments

Comments

@mweststrate
Copy link

mweststrate commented Sep 24, 2018

See: https://github.com/tc39/proposal-decorators/blob/master/TAXONOMY.md#public-prototype-accessor

I noticed that accessors don't support initializer functions. However, in MobX we follow quite closesely the observed semantics as defined in METAPROGRAMMING.md. Except, in our usecase this decorator is typically combined with the class-properties proposal, such that a typical class definition looks like:

class Person {
   @observable age = 13
}

The @observable decorator rewrites the field to an accessor, but that leaves no nice place to run the initializer. Unlike in the example, the initial value is not stored in an additional underlying property, but in a (private) map.

For now we worked around it by running the initializers on the first access to the field, but this evaluates lazily, while ideally we want to keep the semantics of running the initializer eagerly (as-if it was part of the constructor). In order words, I would like to be able to write a transformation along these lines:

// Base descriptor
{
    kind: "field"
    placement: "own",
    key: "age" 
    descriptor: {
        enumerable: true,
        configurable: true,
        writable: true
    },
    initializer() {
         return 17
    }
}

// Desired transformation
function observable(elem) {
    const { key, initializer } = elem
    return {
        kind: "method" // Note: we change the field to an own method to introduce getter / setter
        placement: "own",
        key,
        descriptor: {
            enumerable: true,
            configurable: true,
            get() { 
                this._reportObserved()
                return this._internalStorage.get(key) 
            },
            set(v) {
                this._reportChanged()
                return this._internalStorage.set(key, value) 
            }
        },
        // initializer runs for every new instance, triggering the setter
        // might do some other household stuff as will, like making sure that
        // _internalStorage thing is setup in the first place?
        initializer 
    }
}

// Current transformatipn
function observable(elem) {
    const { key, initializer } = elem
    return {
        kind: "method"
        placement: "own",
        key,
        descriptor: {
            enumerable: true,
            configurable: true,
            get() { 
                // this causes internalStorage to be broken until first read / write!
                // which in turn breaks some reflection / debug utilities
                if (!this._internalStorage.has(key))
                    this._internalStorage.set(key, initializer.call(this))
                this._reportObserved()
                return this._internalStorage.get(key) 
            },
            set(v) {
                this._reportChanged()
                return this._internalStorage.set(key, v) 
            }
        }
    }
}

Apologies for not catching this during earlier reviews!

@mweststrate
Copy link
Author

mweststrate commented Sep 25, 2018

Currently I am using the following workaround, to avoid the laziness problem

// Current transformatipn
function observable(elem) {
    const { key, initializer } = elem
    return {
        key,
        kind: "method",
        placement: "own",
        descriptor: {
            enumerable: true,
            configurable: true,
            get() { 
                reportObserved(this)
                return this._internalStorage.get(key) 
            },
            set(v) {
                reportChanged(this)
                return this._internalStorage.set(key, v) 
            }
        }
        // introduce an additional property that is always undefined,
        // just to be able to rn initialization code upon instance creation
        extras: [{
            key: "__mobx-initializer-" + key,
            kind: "field",
            placement: "own",
            descriptor: {
                enumerable: false,
                configurable: true,
                writable: true
            },
            initializer() {
                if (!this._internalStorage) this._internalStorage = new Map()
                this._internalStorage.set(key, initializer.call(this))
                return undefined
            }
        }]
    }
}

But basicaly this works again around the proposal, by introducing a completely unrelated temporarily property, just to be able to run some instance initialization code. This approach is ugly and leaves the instance in an ugly state (with a bunch of meaningless initializer properties). So what I am looking for is a finisher that runs a little logic on instance creation for every property I introduce

@mweststrate
Copy link
Author

For those interested, this is what the impact of implementing stage2 decorators for MobX now looks like: https://github.com/mobxjs/mobx/blob/6092288297c0cc596064c555ec4203c5c57f5a94/CHANGELOG.md

The good news is that most of our legacy decorators work-arounds can be dropped, and the stage2 implementation is much simpler than the legacy implementation. The only work-around currently still in place is the one described above.

@littledan littledan reopened this Sep 25, 2018
@littledan
Copy link
Member

Would this earlier discussion be related to your need? (You can just read the top post) #44

@mweststrate
Copy link
Author

@littledan yes, that would solve the problem :-)

@mweststrate
Copy link
Author

Closing in favor of #44

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

2 participants