-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Description
Increasing access
The proposed decorator API enables further customisation of p5.js by addons without needing to duplicate or directly modify internal implementation. It is already used internally by FES parameter validation and can provide a route towards additional accessibility oriented features.
Most appropriate sub-area of p5.js?
- Accessibility
- Color
- Core/Environment/Rendering
- Data
- DOM
- Events
- Image
- IO
- Math
- Typography
- Utilities
- WebGL
- Build process
- Unit testing
- Internationalization
- Friendly errors
- Other (specify if possible)
Feature request details
This feature is already partially implemented and in use internally in the latest 2.x release, but it is not officially documented and released publicly yet to work out some details around the implementation. As such this issue is to solicit any additional feedback on the current implementation idea.
Decorations to put simply is a meta-programming technique where one can change or add extra behaviors onto existing classes/functions without needing to know what the underlying implementation is. It provides flexibility to extend existing API while keeping the internal implementation of classes intact.
In p5.js' case, the main use case is in how FES parameter validation works. When a function is called in p5.js, FES will need to look at the parameters being passed and check it against known pattern/types that the function accepts, and in the case where something does not match, FES prints a message. However, to make FES parameter validation work across all functions where available, all functions that wants its parameters checked either needs to manually call FES parameter validation function in its own function body or its function body needs to be modified after the fact. FES in 1.x uses the first option of all function definition includes an explicit call to FES parameter validation, this is extra code that is a bit tedious to maintain and creates additional noise/confusion in implementing functions in p5.js, as a function body should only contain things that the function itself wants to do, FES is more of an external functionality that can be optionally turned off.
In 2.x, FES now uses decorations to dynamically replace the function body to include FES check before also running the original function as defined. I won't go into detail about the previous/current implementation of this but you can refer to the current source as linked at the top if need be. There is an opportunity however to create a public API that not just FES but also any addon libraries can use if they need to tag on additional functionalities to any p5.js function calls, this is where this decorator API comes in.
Proposal
This proposal is partly implementing the existing Stage 3 TC39 Decorators proposal, for details about it do visit the link above. We introduce a new public API, mainly aimed at addon authors of p5.registerDecoration(pattern, decorator) (name subject to change, suggestion welcomed). Unlike the TC39 proposal, the implementation in p5.js needs to be applied at runtime and after all addons are registered but before the p5 instance is created. In addition, there needs to be a way for the user to determine with enough flexibility how and where a decorator should apply.
With these requirements, the API has the following details:
- The
patternargument is a string or a function that returns a boolean value.- If it is a string, it will be matched directly with a "path" and if it matches, the decorator will be applied
- If it is a function, it will be called with a single argument which is an options object that contains the key of
path pathin this case is the name of the class member including all parents until it reachesp5, eg. for theellipse()function, it will be matched with a path ofp5.prototype.ellipse.
- The
decoratorargument is a decorator function that follows the TC39 Decorator proposal and I won't rehash it here other than by providing an example below.
Overall an example use of this API looks like this:
p5.registerDecoration(
({ path }) => {
return path.startsWith('p5.prototype');
},
function(target, { kind, name }){
if(kind === 'method'){
return function(...args){
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
validate(name, args);
}
return target.apply(this, args);
};
}
}
);All public class methods and properties, including static methods and properties and their own members if they are classes, can be decorated, sans caveats below.
Caveats
Due to some limitations within JavaScript that I'm not able to find workaround and the nature of applying decoration at runtime, there are some caveats to this, especially as compared to the TC39 proposal.
- Class fields that are defined in the class body cannot be decorated but those attached to the class prototype can. This is because class fields defined in class body cannot be iterated nor overwritten through the class prototype.
- Class constructor cannot be decorated. This is because the decoration is applied in the constructor call and thus the constrcutor, ie. class itself, cannot be replaced at that point.
- Decorations are applied on each call to
new p5()instead of on call top5.registerDecoration(). This is because the decorator is expected to be registered in addons but individual addons cannot know the order in which other addons are added in relation to itself, by applying the decoration on call to the constructor, it is known at that point that all addons has been registered and any decoration being applied can also affect things added by other addons regardless of registration order. Each decoration will only be applied once and regardless of how many times the constructor is called. It also cannot be unapplied.
User recommendation
Based on the proposed implementation, there are some recommendation for users of this API.
- The decorator's matching pattern function should be kept relatively minimal and performant, while it does not affect the draw loop performance, it can affect startup time if it does too much computational intensive tasks.
- The decorator function implementation should ideally also be relatively lean since it is adding extra code for each function call to run. If it is possible to know that a class member will not need to be decorated based on information that is only available in the decorator (ie. the matching pattern function cannot exclude it), returning
undefinedfrom the decorator function will retain the existing implementation of the class member. - A user should retain the expected behavior of p5.js APIs as much as reasonable, sketch authors have expectation of p5.js' API behaving in a certain way and unless the addon is clearly intended to change those expectation, it should retain existing behavior and just add additional behaviors.
- Both the matching pattern function and the decorator function must not be asynchronous.
I have a working implementation ready locally and will file a PR for review soon but any comments or thoughts would be great at this point!