-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
[p5.js 2.0 RFC Proposal]: Async/Await setup() #6767
Comments
@limzykenneth this looks good to me. Most people have written dozens of sketches by the time they load external media. Introducing |
One other point to mention: if one is loading multiple files, the behaviour of current p5's An option we can consider that would introduce some magic under the hood is to internally keep track of all the promises created by Another option is to add a helper or something with a slightly nicer API. Maybe: let myJSON, myStrings;
async function preload() {
// Like Promise.all but working on object values instead of array items
{ myJSON, myStrings } = loadAll({
myJSON: loadJSON('something.json'),
myStrings: loadStrings('something.txt')
});
} ...but that could use some work since it's relying on destructuring, which is sort of advances javascript for newcomers. Hopefully other people have some other API ideas? |
@davepagurek Parallel loading is indeed a potential concern for this. I'm initially thinking just leaning into promises if the syntax don't feel too complicated. let img1, img2, img3;
async function setup(){
[img1, img2, img3] = await Promise.all([
loadImage("cat1.jpg"),
loadImage("cat2.jpg"),
loadImage("cat3.jpg")
]);
} There's also the potential issue of error handling which with async/await we'll need to use |
Thanks for pointing this out @davepagurek. It feels like we should hide this complexity if at all possible. Loading a few images quickly (in parallel) should be as straightforward as possible. Unless teachers do a lot of hand waving, the current suggestions would force beginners to learn some mix of JavaScript objects/arrays, destructuring, |
I'm not sure that this is a good idea, but what if we do something like this: let data
async function setup() {
data = await preload({
img1: loadImage('cat1.jpg'),
img2: loadImage('cat2.jpg'),
img3: loadImage('cat3.jpg'),
})
}
function draw() {
image(data.img1, 0, 0)
image(data.img2, 100, 0)
image(data.img3, 200, 0)
} Here, each async function preload(data) {
const result = {}
await Promise.all(Object.keys(data).map(async (key) => {
result[key] = await data[key]
})
return result
} More advanced users could destructure this if they want, but otherwise, they can just leave it as an object. The benefit of using this thing that works like |
Neat, that's definitely getting simpler. Objects (or arrays) filled with function calls seem a little complex, though. How about overloading the // Single image.
let img;
async function setup() {
img = await loadImage("cat.jpg");
}
function draw() {
image(img, 0, 0);
} // Multiple images.
let data;
async function setup() {
data = await loadImage({
img1: "cat1.jpg",
img2: "cat2.jpg",
img3: "cat3.jpg",
});
}
function draw() {
image(data.img1, 0, 0)
image(data.img2, 100, 0)
image(data.img3, 200, 0)
} Compared to the current Could we get full parallelism in a // Image and JSON.
let data;
async function setup() {
createCanvas(400, 400);
data = await loadAll({
img: "cat.jpg",
pos: "position.json",
});
}
function draw() {
background(200);
// Get current position.
let pos = data.pos[frameCount];
// Draw cat.
image(data.img, pos.x, pos.y);
} |
Hi all. There's a lot of creative discussion going on here! I have a different view of this, but I could be wrong. Basically, I think the native syntax is significantly less semantic than the current p5 syntax, and more complex for non-experts. But there might be benefits to this proposal that I don't fully understand yet. On semantic codeWith In any case, they're likely to try to look up the meaning instead of spending a long time guessing, and this is equally fraught. As noted on MDN, "Asynchronous JavaScript is a fairly advanced topic." Tutorials tend to be long and assume multiple prerequisites beginners won't have. And it's not hard to find forums where people say they've read all the articles and videos but still can't understand it. Here's an example, so that we have some direct evidence. Leaving the user to rely on these kinds of articles and tutorials seems problematic for p5, and explaining On simple syntaxSyntactically, With
Learning new syntax is beneficial for users, but as a rule, I think it's probably good to introduce new syntax when it becomes necessary, and no sooner. The current API with On modern syntaxI still need to study this more, but my understanding is that JavaScript originally used nested callbacks to accomplish the programming tasks we're discussing here. But, nesting lots of functions inside other functions resulted in code that was hard to look at (so-called "callback hell"). So, in ECMAScript 2015 (ES6), promises were introduced, and these supported chaining with dot notation. That was more readable but still involved connecting a lot of instructions together. Later, in ECMAScript 2017, Based on all this, using callbacks for these sorts of tasks may be viewed as outdated. But old isn't necessarily bad. The original problem that needed to be solved was that deeply nested functions aren't very readable. That doesn't seem to be a problem with the current API. Am I wrong? I haven't worked with this area of p5 much. Benefits?I wouldn't think that removing |
I'm definitely in the freeCodeCamp's explanation of the
would become something like
For parallelism, we could simplify a bit further by working with // Single image.
let img;
async function setup() {
img = await loadImage("cat.jpg");
}
function draw() {
image(img, 0, 0);
} // Multiple images.
let img1;
let img2;
let img3;
async function setup() {
img1 = await loadImage("cat1.jpg");
img2 = await loadImage("cat2.jpg");
img3 = await loadImage("cat3.jpg");
}
function draw() {
image(img1, 0, 0);
image(img2, 100, 0);
image(img3, 200, 0);
} // Multiple images in parallel.
let img1;
let img2;
let img3;
async function setup() {
let data = await loadImage("cat1.jpg", "cat2.jpg", "cat3.jpg");
img1 = data[0];
img2 = data[1];
img3 = data[2];
}
function draw() {
image(img1, 0, 0);
image(img2, 100, 0);
image(img3, 200, 0);
} // Image and JSON in parallel.
let img;
let pos;
async function setup() {
let data = await loadAll("cat.jpg", "position.json");
img = data[0];
pos = data[1];
}
function draw() {
image(img, pos.x, pos.y);
} |
One benefit of the async syntax is that it means there are less states where you could be looking at a variable in an incomplete form. e.g.: let data
function preload() {
data = loadJSON('data.json')
console.log(data) // just logs {}
}
function setup() {
console.log(data) // logs the actual data now
} vs: let data
async function setup() {
data = await loadJSON('data.json')
console.log(data) // logs the data
} Our Another side effect of having to immediately return an object is that it has to stay an object. This means when you Not sure if these alone are enough to tilt the scales, but I wanted to mention them to paint a complete picture! |
Thanks @nickmcintyre! You make a great point. I totally agree that it's best to seek out the simplest version of The way you handle the second case, where As far as documentation goes, maybe we could have one short explanation of Also, @davepagurek, the first benefit that you mentioned seems like it could be pretty significant, just by itself. I need to look into the second one more to understand the options there, but it definitely seems relevant too. I'm hoping to come back to this when I have more time so that I can understand all the nuances better, but I might actually be in favor of the proposal now. I just need to think more. |
The original proposal mentions keeping the callback syntax if necessary. Does anyone know yet if that'd still be necessary if @nickmcintyre's latest idea is adopted? |
@limzykenneth mentioned the following "unusual use case" which I've edited lightly: function setup() {
loadImage("cat.jpg", successCallback);
createCanvas(400, 400);
circle(200, 200, 100);
}
function successCallback(img) {
image(img, 0, 0);
} Here's what that could look like with promises: // Use .then() to draw a cat when it arrives.
function setup() {
let data = loadImage("cat.jpg");
data.then(drawCat);
createCanvas(400, 400);
circle(200, 200, 100);
}
function drawCat(img) {
image(img, 0, 0);
} // Use .catch() to handle a loading error.
function setup() {
let data = loadImage("cat.jpg");
data.then(drawCat).catch(logError);
createCanvas(400, 400);
circle(200, 200, 100);
}
function drawCat(img) {
image(img, 0, 0);
}
function logError(error) {
console.error("🙀", error);
} Thoughts? |
I'm not entirely sure about this since it does seem to complicate things for beginners. I don't know enough about how things are done behind the scenes so let me know if this is possible, but if we were to truly make this straightforward, can we detect when someone is loading data in the setup() function? If so, then under the hood we handle the async/await instead of making the user do this. This would also be closer to the Processing syntax but I'm not sure if cross compatibility is something we consider when making changes. |
We could "listen" for We could potentially support both by having let img
async function setup() {
img = await loadImage('assets/cat.jpg').promise
createCanvas(500, 500)
} We can be flexible about how you access the promise, either via a property, a method, wrapping it in another function, or making another type of function that returns it directly, like |
I am 100% in the camp of async/await, lack of support has been a paint point for me when teaching p5.js and trying to get students out of the p5-only box. #2154 is also something I have run into often, where loadJSON does not work as a student would expect (arrays and numbers are valid JSON, unless you're using p5!). And, when using p5.js with other libraries (like Tone.js), you will often need to use async/await, and it is good to have support for this in I feel this is quite clean and not that much more difficult than the current let size2d;
async function preload () {
size2d = await load('./canvas-size-2d.json'); // JSON array works finally
console.log(size2d); // student can even log it immediately
}
function setup () {
createCanvas(size2d[0], size2d[1]);
} I think it would be good if the below was functionally equivalent to the above, so that an explicit async function setup () {
const size2d = await load('./canvas-size-2d.json');
createCanvas(size2d[0], size2d[1]);
} I have a module that might be relevant here for reference: load-asset. It uses a single interface rather than explicit functions, which is a departure from p5's current approach, and I think a case could be made for either approach. One benefit of the single interface, is that it just becomes "the thing that has the special async/await syntax" and you can combine several different asset types into the same parallel sequence. async function setup () {
// load all assets in sequence, uses Promise.all
const assets = await load.all([ 'a.png', 'b.json', 'c.csv' ]);
// same, but using a named object pattern
const { normal, diffuse } = await load.all({
normal: 'foo/normal.png',
diffuse: 'foo/diffuse.png'
});
} I think this is pretty easy for students to understand: if a file ends with Something to consider is errors. Try/catch is not something I usually teach in beginner p5 classes. And, most of the time, I don't even bother handling errors: if I can't load my data or images, the whole sketch is pretty useless. I think errors/failures can be handled similarly to how they are now with preload: Currently, if I try to |
If the general opinion is that async/await is an advanced topic for beginners we could consider extension. If the desired behaviour is blocking await, then perhaps this pattern is a good first test case.
|
I like @mattdesl's suggestion of one function There's no need for a separate async function setup () {
const images = await load('a.png', 'b.png', 'c.png');
} Use of objects and object destructuring is too complex for beginners. I don't understand how could p5.js tell if the user is awaiting |
Not sure if this is a good idea or not, but in theory we could change the behaviour depending on whether or not it's called from preload or setup by setting some flag before running preload and setting it to something else after. Supporting both means compromising on some things though:
All of those options are feasible but have different tradeoffs. So I think the first decision we have to make is, do we need to support both the old It's sounding to me like we'll need to support both to stay beginner friendly, so my opinion would be to try to support both, and pick our compromises to try to make it as easy as possible to jump from the beginner-friendly API to the more JS-centric API. |
@davepagurek @nickmcintyre @limzykenneth The existing let normal;
let diffuse;
function preload() {
normal = loadImage('foo/normal.png');
diffuse = loadImage('foo/diffuse.png');
} If the only option to load in parallel in p5.js v2.0 was doing it the following way, that'd cause a huge decrease in accessibility. Should users really need to understand how async/await, object syntax, and object destructuring just to load two images in parallel? I don't think @mattdesl was suggesting that at all. let normal;
let diffuse;
async function setup() {
{ normal, diffuse } = await load({
normal: 'foo/normal.png',
diffuse: 'foo/diffuse.png'
});
} I suggest that |
@mvicky2592 thanks for tagging me. In general, I'm in favor of considering just about any idea as long as long as the discussion is constructive. @mattdesl and I suggested similar syntax for async/await. Here's a snippet from my previous comment: // Image and JSON in parallel.
let img;
let pos;
async function setup() {
let data = await loadAll("cat.jpg", "position.json");
img = data[0];
pos = data[1];
}
function draw() {
image(img, pos.x, pos.y);
} Swap I agree with @mattdesl on the following point:
Here's how I suggested we go about it:
async/await syntax isn't beginner-unfriendly; it's just different. I'd be happy to draft a quick tutorial and/or reference page to demonstrate how it could be explained to beginners. That being said, mixing async paradigms would be confusing to beginners. Borrowing from the Zen of Python, "There should be one-- and preferably only one --obvious way to do it." If async/await is simple enough, which I believe it is, then we should remove @davepagurek it'd be nice if |
We'd only need it if we want to use the |
@nickmcintyre Even doing that with an array is still not as simple as directly assigning vars with Why would we add complexity to the p5 user experience if the end result (loading images in parallel) is the same? This suggestion to remove The existing There are use cases where the new way would be preferable certainly, but the opposite is also true. |
@davepagurek oh oops! Gotcha.
@mvicky2592 to paraphrase @limzykenneth's proposal, async/await is idiomatic JavaScript and would be helpful to people who want to integrate their p5.js sketches with the rest of the JavaScript ecosystem. If the approach can be made simple enough, then it's definitely worth consideration. I'd probably introduce let img;
// You can load files such as images by calling the load() function.
// Doing so requires changing the setup() function a little.
//
// The "async" and "await" keywords let p5.js know that you're
// loading data, so it should wait until the data loads. "async"
// and "await" are always used together.
// "async" goes before the "function" keyword.
async function setup() {
// "await" goes before the call to load().
img = await load("cat.jpg");
}
function draw() {
image(img, 0, 0);
} let img1;
let img2;
// You can load two images by calling load() twice.
async function setup() {
img1 = await load("cat1.jpg");
img2 = await load("cat2.jpg");
}
function draw() {
image(img1, 0, 0);
image(img2, 50, 0);
} let img1;
let img2;
let img3;
let img4;
// If you need to load many images, you can load them at the same
// time by passing all of their paths to load(). When you do, load()
// will return an array.
async function setup() {
let data = await load("cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg");
img1 = data[0];
img2 = data[1];
img3 = data[2];
img4 = data[3];
}
function draw() {
image(img1, 0, 0);
image(img2, 50, 0);
image(img3 0, 50);
image(img4, 50, 50);
} The last use case seems to be the sticking point. For this optimization to work, teachers would need to introduce arrays, at least in passing. @davepagurek @limzykenneth would browser caching make files load pretty quickly if a sketch is run more than once? If so, then parallelism could probably wait until it's really needed; beginners could just call I'm currently teaching creative coding with p5.js to a group of high school students who are beginners. Happy to test any/all of this out next week and report back if that'd be helpful. |
@nickmcintyre Teaching students to load assets sequentially for simplicity's sake is, by your own admission, an anti-pattern they'll need to unlearn later. I concede that for the typical amount of images beginners are loading (probably less than 20) the real-world difference is likely minimal, although It'd be great if someone could do test on that once With |
So I think the current tradeoff being discussed is where we want to compromise between these two stances, one stated concisely by @nickmcintyre:
...and this by @mvicky2592:
some options I see:
Let me know if anyone has any other ideas that are worth considering too! also:
I think so, so the concern would be less for when students are iterating, and more for when showing the sketch to other people. |
@davepagurek Yeah my vote is having Also I assumed since
and yes I agree |
Right, there's a world where we can change that behaviour and make it not return a promise if it's in |
@davepagurek Ah yeah I forgot about that idea. For functions like |
@mvicky2592 just a friendly vibe check, I'd appreciate it if you took a bit more care with phrasing when you disagree with people. Comments like "Not supporting it shouldn't even be considered as an option in my opinion." are needlessly dismissive. Suggesting that people must not understand p5.js' design goals when they propose changes you disagree with is condescending; it made me shut down and want to leave the discussion. |
Teaching approaches differ, but I often use ant-patterns to motivate new concepts. For example, magic numbers on the way to variables, copy/pasting on the way to loops, or defining several similar/related variables on the way to data structures. I've taught parallelism in Scratch to many elementary and middle school students after a couple of lessons. It's pretty painless for beginners to pick up on the idea that multiple things can happen at once. Parallelism is an important concept, and I think it'd actually be beneficial for beginners to learn it explicitly with p5.js. A couple of questions come to mind:
|
Increasing access
Adopting async/await can help with sketch authors' adoption of modern JavaScript syntax and best practices, making their skills more generally transferrable. The overall syntax may be considered to be simpler than the need to use the
preload()
function as well.Which types of changes would be made?
Most appropriate sub-area of p5.js?
What's the problem?
Taking directly from the current RFC
What's the solution?
Data loading functions will all be updated to be fully async using promises (with async/await) while keeping the callback syntax if necessary. setup() will be awaited in the runtime and if the user define it as async functions, the promisified data loading functions can be awaited in them. With an async setup() function, the draw() loop will only start after the setup() function has resolved.
draw() can be defined as an async function as well, although this is not recommended because of the possible timing conflict it may have with requestAnimationFrame. (need more testing to confirm)
In terms of example code, in global mode, to load an image and display it:
In instance mode:
This will extend to all loading functions and can be used by addon libraries simply by making any asynchronous operation
async
.For internal loading functions, the callback functions may be retained, eg.
loadImage(imgSrc, successCallback, failureCallback)
, although a case can be made to not have this and just use the underlying promise instead, eg.loadimage(imgSrc).then(successCallback).catch(failureCallback)
.A working example for this can be seen here.
Pros (updated based on community comments)
Cons (updated based on community comments)
preload()
behaviour will likely stop workingProposal status
Under review
The text was updated successfully, but these errors were encountered: