You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposal is a proposal to introduce decorators that have fewer concepts, and are hopefully easier to implement by JVM authors.
The fundamental difference in comparison is that this proposal does not attempt to process and modify property descriptors on a target. Rather, it recognises that modifying the property descriptors has always been a means to an end for 95% of the decorators: Namely a way to trap reads and writes. To transform values.
The motivation behind this proposal is that we want to trap reads and writes to properties. Very similar to the functionality that Proxies provide so successfully. The main difference is that decorators trap all interactions with the decorated instance, and that an entire 'type' is trapped, rather than specific instances. Things that cannot be done by Proxies.
The rest of the proposal is a proposal to achieve the "property trapping". Probably any other proposal would work for me personally as well. If engine or library authors want to adjust this proposal for whatever benefit: I think the wiggle room is there, as long as it can achieve the goal of trapping property reads and writes per instance, but being able to declare such traps for an entire type.
A big benefit of traps is that they often avoid the need of introducing additional properties. E.g. if there is a getter / setter to normalize input or make some assertions, currently it is needed to introduce an additional property to store the getted / setted value. With traps. this can collapsed into one property, avoiding the need of patterns like _name = "x"; get name() { return this._name }} etc.
The proposal
Property traps
A property trap is an object that provides two functions, named get and set. The goal of these traps is to be able to trap any read and write to a property. Traps can be used to achieve two effects
A decorator syntactically is a prefix of a class or object member.
Syntactically: @<DECORATOR1> <...DECORATORN> <MEMBERDECLARATION>
Everyone of those expressions is evaluated and should resolve a function that returns a PropertyTrap. The functions get's passed in the target that will receive the property definition, and the property name. In other words, the signature of a field decorator is:
If a field decorator does not return a trap, the decorator does not install additonal traps. However, the decorator could still be valuable as it can have side effects, such as storing meta data. (example: @serializable, @deprecated etc)
A first example
functionlogged(){return{get(target,instance,property,value){console.log(`GET`,target,instance,property,value)returnvalue},set(target,instance,property,value){console.log(`SET`,target,instance,property,value)returnvalue}}}classC{
@loggedx=3}// (1) SET C.propotype undefined 'x' 3constc=newC()//c.x// (2) GET C.prototype c 'x' 3c.x=4// (3) SET c c 'x' 4c.c// (4) GET c c 'x' 4
Semantic notes:
When construction the initial property descriptor, the traps are already applied, so that means that the @logged traps are applied during the construction of member x on class C above, and it is the transformed value that ends up in the initial property descriptor. Which is the reason we can observe side effect (1) above.
In the traps target represents the (to be) owner of the property descriptor that is being created. instance represents the this context that is used during a property access. When constructing C.x above, there is a target (C.prototype) but not a this context, hence no instance argument.
On the first read, the read is handled (or intercepted upon) the C.prototype, and the context is c, which we can observe in the output of (2).
When writing to c.x, this will result in a new property descriptor on the instance c. This is again represented in the arguments passed to the traps. Note that again the traps are applied during the computation of the new descriptor for c (the traps are inherited, similar to other properties of the original property descriptor, such as enumerable and writeable).
Note that the original decorator expression @logger is not re-evaluated again! Rather, the traps in which this originally resulted are reused and inherited by the new property descriptor (see below).
If we read from c.x again, this read is now handled by the prop descriptor on the instance, which is reflected in the arguments passed to the traps: the property owner equals the this now.
Storing traps
Traps are stored as the traps property on a PropertyDescriptor. When copying a property descriptor from one object to another, those traps are preserved. So in the above example the following would hold:
(Note, could also be stored somewhere else, as long as semantics are similar to here, and traps can be requested through reflection)
Reflection
As shown above traps are detectable trough reflection (this is different from Proxies).
Also, traps can be by-passed at any time, for example:
Object.getOwnPropertyDescriptor(c,"x").value// prints 3, without the side effects of logging
This is intentional: it makes sure that developer tools such as the debugger don't accidentally trigger side effects, makes it easy to inspect the underlying data structures, and provides an escape hatch from the traps when needed (either by libraries or users). it is very well conceivable that traps themselves use this trick to bypass themselves (e.g. a set trap might use this to get the original value, but not trigger the side effect of it's corresponding get trap)
Getters and setters
Traps are just pass-through things, so they don't necessarily operate on just value based property descriptors, but work for get / set based descriptors as well. Except that the last category will not hit the traps when the property is originally declared (since there is no value to be initialized)
Trap initializer
Decorators can be parameterized by creating functions that return decorators. For example:
Note again that in the property descriptor for obj.x the resolved value of the decorator expression is stored, in this case a logger trap object where enabled=true is trapped in its closure.
Chaining
Decorator can be chained, which means that all the traps are applied from inside out (or, right to left):
constobj={
@private @loggedx: 5}
In the traps property of the descriptor this results in an array with the order of the traps as they will be applied:
Class and function decorators can be large kept as they are currently implemented in babel and typescript
For example @a class X is desugared to const X = a(class X) just as currently implemented already by babel-plugin-legacy-decorators and typescript's experimentalDecorators flag.
Two open questions raised by this, on which I don't have a strong opinion:
is a class decorator a side effect, or can it potentially return something different? I prefer the second; it is a little more flexible, and it makes clear that you ought to to be doing export @something class (see next question)
what is the impact on hoisting? Will decorating a class / function implicitly kill the hoisting? Should class / function decorators be only side effect full to prevent that?
(if somebody could point me to a more accurate write down of the currently implemented semantics, that would be great)
Engine impact (and summary of the proposal)
This proposal tries to keep the impact on JS engines very limited:
Something something parsing
Whenever a property descriptor is being created for an object literal or class member, the following happens:
the decorator expressions are evaluated. This should result in a function
the function is called with the object that will receive the property, and the property name. The return of that function should be a trap or nothing
(if applicable) the set handlers of the traps this results in are applied to the value the property descriptor would be initialized with
(if applicable) the value this results in is stored in the value slot of the descriptor
the set of traps is stored in the traps slot of the descriptor (only if there were any)
Whenever we write to an object: if there are traps stored in the descriptor that receives the write, the set handlers of those traps are applied first, and the resulting value is stored in .value of the descriptor / passed to the set handler of the (potentially new) descriptor
When reading from an object: if there are traps stored in the descriptor that receives the read, the value that is stored in .value of the descriptor (or: the value that is returned from the get handler of the descriptor) is passed trough all get traps of the traps stored in the descriptor
Profit
Reference examples
Not all examples of the current readme are here:
@defineElement decorating entire classes is not in this proposal (see below).
@metaData didn't work it out, but should be doable
@frozen, @callable applies to a class, so skipped
@set cannot be done in this proposal. So field initializers in subclasses would need to receive the same decorators as the superclass if needed.
constallClasses=newMap()// string -> constructorconstallMembers=newMap()// klass -> propName[]functionmetaData(target,propName?){if(arguments.length===1){// used on classallClasses.set(target.constructor.name,target)returntarget}else{allMembers.set(target,[...(allMember.get(target)??[]),propName])}}
@metaDataclassX{
@metaDatamember1
@metaDatafunction(){}}
functionbound(){return{get(target,instance,property,fn){// happens when calling Foo.method for example in example belowif(!instance)returnfn// remember that traps get inherited? // if we bound before, this trap doesn't have to do anything anymore // (we could skip storing the bound methods below, but better cache those bound methods)if(target===instance)returnfn// target is not the instance, we still need to bindconstbound=fn.bind(instance)instance[property]=boundreturnbound}}}classFoo{x=1;
@boundmethod(){console.log(this.x);}queueMethod(){setTimeout(this.method,1000);}}newFoo().queueMethod();// will log 1, rather than undefined
functiontracked(){return{set(t,i,p,v){// Note that we can't render synchronously, // as the new value would otherwise not be visible yet!setImmediate(()=>this.render())// In practice, what would happen in MobX etc is that they // would mark this object as 'dirty', and run render// at the end of the current event handler, rather than awaiting a next tick// alternatively, it would be possible to write this value// to _another_ property / backing Map, like done in the Readme, so that the new value is externally visible before this chain of traps ends, as done belowreturnv}}}classElement{
@trackedcounter=0;increment(){this.counter++;}render(){console.log(counter);}}conste=newElement();e.increment();e.increment();// logs 2// logs 2
@syncTracked
functionsyncTracked(){return{set(t,instance,prop,value){instance["_"+prop]=value// or use a Mapthis.render()returnundefined// booyah what is stored in *this* prop}get(t,instance,prop,value){returninstance["_"+prop]}}}classElement{
@syncTrackedcounter=0;increment(){this.counter++;}render(){console.log(counter);}}conste=newElement();e.increment();e.increment();// logs 2// logs 2
Non goal: modify signature
In previous decorator proposal, it is possible to significantly modify the property descriptor, or even the entire shape of the thing under construction. That is not the case with this proposal; decorators can not influence where the descriptor ends up, it's enumerability, configurability, it's type etc.
The only thing that the decorator can influence is
The initial value of the property descriptor (except for getters / setters)
As side effect it could introduce other members on the target, however, this is considered bad practice and is typically best postponed until we trap a read / write with a known instance.
Non goal: run code upon instance initialization
This proposal doesn't offer a way to run code guaranteed when construction a new instance, although code might run when a trap is hit during a read or write in the constructor
TypeScript
Traps don't modify the type signature at this moment. Technically, they can be used to divert the type of x.a from Object.getOwnProperty(x, "a").value, but that doesn't seem to be an interesting case to analyse statically.
More interestingly, traps can be used to normalize arguments, which means that the type being read could be narrow than the type to which is written. For example:
constasInt=()=>({set(t,i,p,value: any): number{if(typeofvalue==="number")returnvaluereturnparseInt(value)}})constperson={
@asIntage="20"}typeofperson.age/// number
In the above example, the "write type" of age property would be any, while the read type would be number. However, TS can at the moment not distinguish between the readable and writeable property, and the same limitation already applies to normal getters and setters.
Edge cases
It is possible to omit set or get from a trap. The absence of such trap represents a verbatim write-trough (just like Proxies). See also the asInt example above
Instead of storing the decorators on the property descriptor, it probably suffices to be at least able to detect them trough reflection, e.g.: Reflect.getOwnDecorators(target, property): PropertyTrap[]
Optimization notes
Like proxy handers, property traps are shared as much as possible, so they are not bound to a specific instance, type or property, but rather receive those as arguments.
An assumption in this proposal is that when writing to a property that is defined in the prototype, the original descriptor is copied onto the instance, and we could just as well copy the traps with it. However, if this assumption is wrong, this proposal doesn't strictly require this copy-traps mechanism; as the trap on the prototype could take care itself of the copying the traps in cases where this is need (for example, in @tracked it is, but in @bound it isn't).
The reason that both the decorator and the traps themselve receive the target is that this makes it much easier to reuse traps among different properties and classes. Probably this will be much better optimizable, just like proxy traps are a lot more efficient as soon as the handlers are reused. So for example, it would be adviseable to hoist traps from the decorator definitions whenever possible:
constloggerTraps={get(target,instance,property,value){console.log(`GET`,target,instance,property,value)returnvalue},set(target,instance,property,value){console.log(`SET`,target,instance,property,value)returnvalue}}functionlogger(){returnloggerTraps}classX{
@loggermethodA(){}// both methods now use the same logging trap
@loggermethodB(){}}
Note that this optimization notice applies to most examples above
update 27-nov-2019:
clarified / linked to the old class decoration proposal
changed the signature to of decorators to be a function, to make it easier to do meta-data-only decorators
added examples for @frozen, @defineElement, @metaData
Trapping decorators
Motivation
This proposal is a proposal to introduce decorators that have fewer concepts, and are hopefully easier to implement by JVM authors.
The fundamental difference in comparison is that this proposal does not attempt to process and modify property descriptors on a target. Rather, it recognises that modifying the property descriptors has always been a means to an end for 95% of the decorators: Namely a way to trap reads and writes. To transform values.
The motivation behind this proposal is that we want to trap reads and writes to properties. Very similar to the functionality that Proxies provide so successfully. The main difference is that decorators trap all interactions with the decorated instance, and that an entire 'type' is trapped, rather than specific instances. Things that cannot be done by Proxies.
The rest of the proposal is a proposal to achieve the "property trapping". Probably any other proposal would work for me personally as well. If engine or library authors want to adjust this proposal for whatever benefit: I think the wiggle room is there, as long as it can achieve the goal of trapping property reads and writes per instance, but being able to declare such traps for an entire type.
A big benefit of traps is that they often avoid the need of introducing additional properties. E.g. if there is a getter / setter to normalize input or make some assertions, currently it is needed to introduce an additional property to store the getted / setted value. With traps. this can collapsed into one property, avoiding the need of patterns like
_name = "x"; get name() { return this._name }}etc.The proposal
Property traps
A property trap is an object that provides two functions, named
getandset. The goal of these traps is to be able to trap any read and write to a property. Traps can be used to achieve two effectsThe signature of a
PropertyTrapobject is:Field Decorators
A decorator syntactically is a prefix of a class or object member.
Syntactically:
@<DECORATOR1> <...DECORATORN> <MEMBERDECLARATION>Everyone of those expressions is evaluated and should resolve a function that returns a PropertyTrap. The functions get's passed in the target that will receive the property definition, and the property name. In other words, the signature of a field decorator is:
If a field decorator does not return a trap, the decorator does not install additonal traps. However, the decorator could still be valuable as it can have side effects, such as storing meta data. (example:
@serializable,@deprecatedetc)A first example
Semantic notes:
When construction the initial property descriptor, the traps are already applied, so that means that the
@loggedtraps are applied during the construction of memberxon classCabove, and it is the transformed value that ends up in the initial property descriptor. Which is the reason we can observe side effect(1)above.In the traps
targetrepresents the (to be) owner of the property descriptor that is being created.instancerepresents thethiscontext that is used during a property access. When constructingC.xabove, there is atarget(C.prototype) but not athiscontext, hence noinstanceargument.On the first read, the read is handled (or intercepted upon) the
C.prototype, and the context isc, which we can observe in the output of(2).When writing to
c.x, this will result in a new property descriptor on the instancec. This is again represented in the arguments passed to the traps. Note that again the traps are applied during the computation of the new descriptor forc(the traps are inherited, similar to other properties of the original property descriptor, such asenumerableandwriteable).Note that the original decorator expression
@loggeris not re-evaluated again! Rather, the traps in which this originally resulted are reused and inherited by the new property descriptor (see below).If we read from
c.xagain, this read is now handled by the prop descriptor on the instance, which is reflected in the arguments passed to the traps: the property owner equals thethisnow.Storing traps
Traps are stored as the
trapsproperty on a PropertyDescriptor. When copying a property descriptor from one object to another, thosetrapsare preserved. So in the above example the following would hold:(Note, could also be stored somewhere else, as long as semantics are similar to here, and traps can be requested through reflection)
Reflection
As shown above traps are detectable trough reflection (this is different from Proxies).
Also, traps can be by-passed at any time, for example:
This is intentional: it makes sure that developer tools such as the debugger don't accidentally trigger side effects, makes it easy to inspect the underlying data structures, and provides an escape hatch from the traps when needed (either by libraries or users). it is very well conceivable that traps themselves use this trick to bypass themselves (e.g. a
settrap might use this to get the original value, but not trigger the side effect of it's correspondinggettrap)Getters and setters
Traps are just pass-through things, so they don't necessarily operate on just
valuebased property descriptors, but work forget / setbased descriptors as well. Except that the last category will not hit the traps when the property is originally declared (since there is no value to be initialized)Trap initializer
Decorators can be parameterized by creating functions that return decorators. For example:
Note again that in the property descriptor for
obj.xthe resolved value of the decorator expression is stored, in this case a logger trap object whereenabled=trueis trapped in its closure.Chaining
Decorator can be chained, which means that all the traps are applied from inside out (or, right to left):
In the
trapsproperty of the descriptor this results in an array with the order of the traps as they will be applied:Classes and function decorators
Class and function decorators can be large kept as they are currently implemented in babel and typescript
For example
@a class Xis desugared toconst X = a(class X)just as currently implemented already bybabel-plugin-legacy-decoratorsand typescript'sexperimentalDecoratorsflag.Two open questions raised by this, on which I don't have a strong opinion:
export @something class(see next question)(if somebody could point me to a more accurate write down of the currently implemented semantics, that would be great)
Engine impact (and summary of the proposal)
This proposal tries to keep the impact on JS engines very limited:
sethandlers of the traps this results in are applied to the value the property descriptor would be initialized withvalueslot of the descriptortrapsslot of the descriptor (only if there were any)sethandlers of those traps are applied first, and the resulting value is stored in.valueof the descriptor / passed to thesethandler of the (potentially new) descriptor.valueof the descriptor (or: the value that is returned from thegethandler of the descriptor) is passed trough allgettraps of thetrapsstored in the descriptorReference examples
Not all examples of the current readme are here:
@defineElementdecorating entire classes is not in this proposal (see below).@metaDatadidn't work it out, but should be doable@frozen,@callableapplies to a class, so skipped@setcannot be done in this proposal. So field initializers in subclasses would need to receive the same decorators as the superclass if needed.@defineElement
@metadata
@logger
@Frozen
@bound
@Tracked
@syncTracked
Non goal: modify signature
In previous decorator proposal, it is possible to significantly modify the property descriptor, or even the entire shape of the thing under construction. That is not the case with this proposal; decorators can not influence where the descriptor ends up, it's
enumerability,configurability, it's type etc.The only thing that the decorator can influence is
valueof the property descriptor (except for getters / setters)target, however, this is considered bad practice and is typically best postponed until we trap a read / write with a knowninstance.Non goal: run code upon instance initialization
This proposal doesn't offer a way to run code guaranteed when construction a new instance, although code might run when a trap is hit during a read or write in the constructor
TypeScript
Traps don't modify the type signature at this moment. Technically, they can be used to divert the type of
x.afromObject.getOwnProperty(x, "a").value, but that doesn't seem to be an interesting case to analyse statically.More interestingly, traps can be used to normalize arguments, which means that the type being read could be narrow than the type to which is written. For example:
In the above example, the "write type" of
ageproperty would beany, while the read type would benumber. However, TS can at the moment not distinguish between the readable and writeable property, and the same limitation already applies to normal getters and setters.Edge cases
It is possible to omit
setorgetfrom a trap. The absence of such trap represents a verbatim write-trough (just like Proxies). See also theasIntexample aboveInstead of storing the decorators on the property descriptor, it probably suffices to be at least able to detect them trough reflection, e.g.:
Reflect.getOwnDecorators(target, property): PropertyTrap[]Optimization notes
Like proxy handers, property traps are shared as much as possible, so they are not bound to a specific instance, type or property, but rather receive those as arguments.
An assumption in this proposal is that when writing to a property that is defined in the prototype, the original descriptor is copied onto the instance, and we could just as well copy the traps with it. However, if this assumption is wrong, this proposal doesn't strictly require this copy-traps mechanism; as the trap on the prototype could take care itself of the copying the traps in cases where this is need (for example, in
@trackedit is, but in@boundit isn't).The reason that both the decorator and the traps themselve receive the target is that this makes it much easier to reuse traps among different properties and classes. Probably this will be much better optimizable, just like proxy traps are a lot more efficient as soon as the handlers are reused. So for example, it would be adviseable to hoist traps from the decorator definitions whenever possible:
Note that this optimization notice applies to most examples above
update 27-nov-2019:
@frozen,@defineElement,@metaData