processing / p5.js

p5.js is a client-side JS platform that empowers artists, designers, students, and anyone to learn to code and express themselves creatively on the web. It is based on the core principles of Processing. http://twitter.com/p5xjs —
http://p5js.org/
GNU Lesser General Public License v2.1
21.11k stars 3.22k forks source link

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

Closed nickmcintyre closed 1 week ago

nickmcintyre commented 1 week ago

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?

Most appropriate sub-area of p5.js?

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)

Cons (updated based on community comments)

TBD

Proposal status

Under review

limzykenneth commented 1 week ago

@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 commented 1 week ago

@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.

mvicky2592 commented 1 week ago

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 commented 1 week ago

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.