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

[p5.js 2.0 RFC Proposal]: Promises #7100

Closed
8 of 21 tasks
nickmcintyre opened this issue Jun 20, 2024 · 4 comments
Closed
8 of 21 tasks

[p5.js 2.0 RFC Proposal]: Promises #7100

nickmcintyre opened this issue Jun 20, 2024 · 4 comments

Comments

@nickmcintyre
Copy link
Member

nickmcintyre commented Jun 20, 2024

Increasing access

Asynchronous JavaScript can be confusing, especially for beginners. A simple, consistent, and standard programming model could help to smooth out everyone's learning curve.

Which types of changes would be made?

  • Breaking change (Add-on libraries or sketches will work differently even if their code stays the same.)
  • Systemic change (Many features or contributor workflows will be affected.)
  • Overdue change (Modifications will be made that have been desirable for a long time.)
  • Unsure (The community can help to determine the type of change.)

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)

What's the problem?

p5.js' asynchronous programming model currently uses callbacks. It works, but most of the JavaScript ecosystem has migrated to Promises because they're easier to reason about, especially when used with async/await.

What's the solution?

I suggest that we use Promises and async/await consistently across the API. Most asynchronous functions uses Promises internally, so the implementation would probably be straightforward.

Here's an example of loading an image based on feedback in #6767:

// Load a cat.
let img;

async function setup() {
  img = await load("cat.jpg");
}

function draw() {
  image(img, 0, 0);
}
// Load two cats.
let img1;
let img2;

async function setup() {
  img1 = await load("cat1.jpg");
  img2 = await load("cat2.jpg");
}

function draw() {
  image(img1, 0, 0);
  image(img2, 50, 0);
}
// Use .then() to draw a cat when it arrives.

function setup() {
  let data = load("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 = load("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);
}

And here's a possible revamp for httpGet() based on feedback in #7090:

async function setup() {
  let earthquakes = await httpGet("https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&limit=1&orderby=time");
  
  background(200);
  let earthquakeMag = earthquakes.features[0].properties.mag;
  let earthquakeName = earthquakes.features[0].properties.place;
  circle(width / 2, height / 2, earthquakeMag * 10);
  textAlign(CENTER);
  text(earthquakeName, 0, height - 30, width, 30);
}
// Use .then() to draw a circle when the earthquake data loads.

function setup() {
  let data = httpGet("https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&limit=1&orderby=time");
  data.then(drawEarthquake);
}

function drawEarthquake(earthquakes) {
  background(200);
  let earthquakeMag = earthquakes.features[0].properties.mag;
  let earthquakeName = earthquakes.features[0].properties.place;
  circle(width / 2, height / 2, earthquakeMag * 10);
  textAlign(CENTER);
  text(earthquakeName, 0, height - 30, width, 30);
}
// Use .catch() to handle a loading error.

function setup() {
  let data = httpGet("https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&limit=1&orderby=time");
  data.then(drawEarthquake).catch(logError);
}

function drawEarthquake(earthquakes) {
  background(200);
  let earthquakeMag = earthquakes.features[0].properties.mag;
  let earthquakeName = earthquakes.features[0].properties.place;
  circle(width / 2, height / 2, earthquakeMag * 10);
  textAlign(CENTER);
  text(earthquakeName, 0, height - 30, width, 30);
}

function logError(error) {
  console.error("🆘", error);
}

Note: This already works.

Parallelism

There's an open question about the best way to handle parallelism. preload() currently manages this with magic, which is nice. A proposed solution for async/await would use Promse.all() behind the scenes and return an array:

let img1;
let img2;
let img3;
let img4;

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);
}

Or:

let cats;

async function setup() {
  cats = await load("cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg");
}

function draw() {
  image(cats[0], 0, 0);
  image(cats[1], 50, 0);
  image(cats[2] 0, 50);
  image(cats[3], 50, 50);
}

Or, treading lightly here:

let img1;
let img2;
let img3;
let img4;

async function setup() {
  [img1, img2, img3, img4] = await load("cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg");
}

function draw() {
  image(img1, 0, 0);
  image(img2, 50, 0);
  image(img3 0, 50);
  image(img4, 50, 50);
}

This optimization probably isn't needed for iterative work, but it's definitely helpful for sharing sketches. My sense is that beginners are usually ready for arrays by the time they need to draw a litter of kittens.

Pros (updated based on community comments)

  • Consistency: TBD
  • Readability: TBD

Cons (updated based on community comments)

TBD

Proposal status

Under review

@limzykenneth
Copy link
Member

@nickmcintyre Sorry I'm not super sure how this differs from #6767 in that they both are about implementing promise/async/await based loading?

@nickmcintyre
Copy link
Member Author

@limzykenneth oops, definitely worth clarifying. I thought it might be helpful to lightly decouple the discussion about async setup() and preload() from a discussion about keeping callbacks or fully adopting Promises (i.e., .catch() and .error()). They're closely related, but the latter hasn't really been addressed in #6767. So, I created a space for it.

@github-project-automation github-project-automation bot moved this to Completed in p5.js 2.0 Jun 20, 2024
@mvicky2592
Copy link

most of the JavaScript ecosystem has migrated to Promises

This is mostly true but callbacks are still commonly used in JS event and array functions.

@davepagurek
Copy link
Contributor

I guess to clarify, callbacks for non-asynchronous cases (e.g. arrays) and ones without a single event to listen to (e.g. DOM event listeners) are still the standard. For all cases where promises are appropriate (asynchronous and involve waiting on a single future event) it seems that core js APIs have moved to promises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Completed
Development

No branches or pull requests

4 participants