Open calebfoss opened 7 months ago
.forEach
and not for loops. Is what you mean here being that to use for...of
instead of .forEach
?Good point. I actually made the same suggestion to match the p5.js Web Editor's style. My understanding is that Prettier was removed as a dev dependency for performance reasons. We stuck with a modified version of the Airbnb JavaScript guide, which uses single quotes. I agree we should aim for consistency.
I disagree about readability, but I hear your point about smoothing out the learning curve. How about we linger here for a moment? Here are some code snippets to compare different iteration scenarios:
Simple iteration
for (let mover of movers) {
mover.update();
mover.render();
}
movers.forEach((mover) => {
mover.update();
mover.render();
});
Using an index
for (let i = 0; i < movers.length; i += 1) {
let x = i * 10;
let y = 20 * sin(x * 0.01) + 200;
movers[i].pos(x, y);
movers[i].update();
movers[i].render();
}
movers.forEach((mover, i) => {
let x = i * 10;
let y = 20 * sin(x * 0.01) + 200;
mover.pos(x, y);
mover.update();
mover.render();
});
Updating
for (let i = 0; i < sizes.length; i += 1) {
sizes[i] += 1;
}
sizes = sizes.map((size) => size + 1);
Everyone's learning path is different, but it's probably safe to assume that someone programming with arrays could pick up .forEach()
pretty quickly. Our intro chapter will teach arrays after functions, basic OOP, and loops. .forEach()
and other array methods fit pretty naturally into that scope and sequence.
All that said, it seems like @calebfoss suggested a more general principle of using syntax with the fewest prerequisites (please feel free to correct me). @katlich112358 @dkessner @msqcompsci @limzykenneth @davepagurek @Qianqianye what do y'all think about array methods? Are there any other opportunities to simplify the the code style guide?
The point about having the index is interesting. With a for...of
loop, the index does not come directly but it is possible for the user to use arr.entries()
with a full for (const [i, val] of arr.entries())
although in this case the user will need to know about .entries()
and array desctructuring for [i, val]
which it is worth thinking about how it compares with .forEach((val, i) => {})
.
On the other hand, for...of
have a couple possible advantages over .forEach
in that 1. you can break
or continue
; 2. await
will work as expected.
I don't have a super strong opinion on this one, both .forEach
or for..of
have their advantages and I'm happy to use either in examples.
One minor bit to add to the discussion: I'm not sure about everyone else, but even though I know the difference between for..in
and for..of
, I find it harder sometimes to spot one vs the other when reading code, as both read very similarly in English. I'm not sure if we have any examples that loop over both object keys and arrays, but in that case, Object.keys(array).forEach(...)
is more verbose but maybe clearer to show that something different is happening.
Thank you for the discussion! I appreciate y'all taking the time.
I wanted to clarify a couple things. I personally really like using array methods and find them quite readable if written well, but speaking as an instructor these are the aspects of using array method that would present obstacles for my students:
My students would have already learned about for loops, so that syntax structure would be familiar. Honestly, if the p5 code examples use array methods, as an instructor, I would most likely show my students a modified version that uses the tools they've already learned.
I wrote a little add-on with a Python-esque range function to help students with numerical iteration. It could also be used for updating array values, since for... of doesn't allow for that:
for(let index of range(sizes.length)) {
sizes[index] += 1;
}
I was thinking about proposing that function for the base p5 library, but I'll put that in a new thread. A big benefit is that infinite loops are impossible, which is a huge plus for beginners.
@davepagurek Good point about the "in" vs "of" confusion. That is a notable downside.
@limzykenneth Good point about break, continue, and await. One annoying thing about iterating over the entries method is that the indices are strings. It's not a huge issue, but something that caught me off guard that I wanted to mention.
Hearing the pros and cons for different options makes me wonder - maybe the style guide could include multiple and suggest different ways of iterating for different situations? Using only array methods would be more consistent, and if we're sticking with that, I'll make sure all the relevant examples use them. Again, I would just work around that when I'm teaching.
One annoying thing about iterating over the entries method is that the indices are strings
I just did a quick test, it looks like they're numbers here at least?
Iterating over the indices of an array with in
definitely makes strings though:
One annoying thing about iterating over the entries method is that the indices are strings
I just did a quick test, it looks like they're numbers here at least?
Iterating over the indices of an array with
in
definitely makes strings though:
Oops! Thanks for checking.
Also just to contradict myself a bit and confuse things - the same thing I said about => being a new symbol would be true about the keyword "of". My opinion is that using "of" is much simpler to learn, and the syntax involved is much simpler. As an English speaker, though, I'm biased towards English keywords over abstract symbols.
A more verbose approach that would avoid arrow functions would be:
function addOne(size, index) {
sizes[index] = size + 1;
}
sizes.forEach(addOne);
function addOne(size, index) {
sizes[index] = size + 1;
}
sizes.forEach(addOne);
Just a quick note that this style is also what I see Dan Shiffman do in the beginner oriented videos for Coding Train, albeit for event handlers, so it's not out of the question.
function addOne(size, index) { sizes[index] = size + 1; } sizes.forEach(addOne);
Since it doesn't look like this goes against the style guide as-is, I'm thinking this would be a good format to use in the examples. We have already examples with named functions being passed into DOM element event methods, so this would be more familiar. If anyone sees any issues with that, please let me know. If not, it might be worth including this style in the style guide as an alternative.
I think this is OK as long as we keep the function and its use side-by-side like in these snippets so that people can still read the code linearly without jumping around too much, as opposed to placing them next to globals like draw
.
For me it is still a bit hard to decide which to go for. In everyday coding I instinctively use .forEach
with inline closure just because I personally like the syntax but when later I found out I need to await
, I'll be converting it into for...of
. I don't know really is the main takeaway.
One thing to note also is that we might do some edits to the style guide later on to clarify differences between example code and p5.js source code style, ie. example code style are designed to be consistent, simple, and beginner friendly; while the p5.js source code style may have more in the ways of case by case analysis, small tricks, and optimizations when necessary. One example is const
and let
, example code only uses let
while p5.js source code uses const
whenever possible and only use `let if reassignment is needed.
Forgot to add,
sizes.forEach(function(size){
size = size + 1;
});
is also possible if the desire is to avoid arrow syntax, as long as this loop is not used in object methods.
That's a great idea to distinguish between examples and source code. For examples, I think beginners would find it easier to read with the function declared outside the method call, which is why I wrote it that way, even though I'd personally never write code that way. In my experience, the more nested parentheses and braces, the more students/beginners get stuck on syntax issues that hold them back from doing what they want.
In my experience, the more nested parentheses and braces, the more students/beginners get stuck on syntax issues that hold them back from doing what they want.
I think that's the essential point. Arrow functions are dope, but they're definitely an intermediate concept with enough nuance to trip people up. I also agree that most beginners and their instructors are more comfortable with some variation of a for
loop.
What do y'all think about the following?
Array iteration
// Updating.
for (let i = 0; i < sizes.length; i += 1) {
sizes[i] += 1;
}
// Not updating.
for (let size of sizes) {
circle(200, 200, size);
}
Callbacks
// Bad.
button.mouseClicked(() => {
thingOne();
thingTwo();
});
// Good.
button.mouseClicked(handleClick);
function handleClick() {
thingOne();
thingTwo();
}
@nickmcintyre I'm in favor of that of course. I must admit that I went into this discussion only thinking about documentation and not source code. I don't see any issues with using arrow functions passed into array methods in source code. The only one I might be cautious of is reduce, which can become super confusing to read if not written carefully. If we're sticking with one style guide for both source code and documentation though (at least for now), what you have there looks great to me.
Oops. To clarify, my style suggestions are focused on documentation.
I don't actively contribute to the p5.js source code (yet), so I don't really have an opinion there. There's probably an upside to adhering to something standard-ish such as Airbnb for most situations.
Great discussion! Somehow I missed this issue but found it when @nickmcintyre mentioned it in #6527. I think there's a case to be made for using only traditional for
loops in the p5.js reference examples. I'll tread lightly since I seem to be a minority of one here, but I think there's a solid case to be made for this approach! (Sorry in advance that this answer is so long. I tried to edit it down I swear!)
for
in all casesUsing only for
loops reduces the prerequisite knowledge required to understand the documentation. In particular, it means beginner's don't need to learn more than what's in the Foundation section of the p5.js reference, which contains for
but not for...of
or forEach()
. I think a good rule of thumb may be to write code examples that leverage only the language features included in the reference, when possible, since p5.js is often used as a first introduction to programming. (I'm cheating slightly here, since the .length
property of arrays doesn't seem to be in the reference, but I think that's less of a reach for beginners.)
It reduces prerequisite knowledge because beginners already need to know for
loops to iterate in general (not just over arrays). We can see this reflected in the style guide, since the section on iteration opens by saying "Use for
loops to iterate a fixed number of times." If the style guide's section on iteration simply ends at "Use for
loops," it becomes simpler for the contributors following the guide and for the beginners reading a reference that follows the guide. We could still add a remark for source-code contributors about more nuanced guidelines.
While it may seem like a small step to learn .forEach()
or for...of
, I think it's important to not underestimate the curse of knowledge. As a concrete example, experts may feel callback functions are easy to explain, but they can be hard for beginners and they're not necessary for iterating over arrays. I think they're okay for the event handling portion of the p5 API because in that case they're harder to avoid, and because that's a more advanced topic.
After many years of tutoring math (and some programming), I still need to remind myself to be careful about the curse of knowledge. Instinctively, I still feel like I can explain a concept in a few minutes, even though in reality, it usually takes ten times as long.
I'll give an example from my most recent tutoring session. We were working on a math problem, but the situation was nearly identical to the programming problem we're discussing now, which is to figure out how to iterate over an array. I identified three approaches for the solution. Among them, one solution was by far the fastest, simplest, and best suited to the problem. But the student found it difficult to implement because it was new. They had an easier time with a longer, indirect approach based on something they had already learned. This isn't actually surprising because beginners need to do a lot of hard things in order to use a new technique:
.forEach
instead of a for
loop).Without chunking, a beginner's fragile working memory can be exhausted by the details of a single approach, making it hard to decide between multiple approaches (like for
vs. for...of
). All of this is especially difficult in the beginning because they might still be shaky on the first approach they learned; introducing new approaches too early may exacerbate this since they may get insufficient practice with the first approach.
Because of these kinds of issues, I tend to favor an emphasis on the irreducible minimum of core concepts: variables, basic data types and structures, control flow (if...else
and for
), and functions (not including arrow functions). It's possible to get very far with just these ideas, and I suspect that using the same syntax consistently will prevent certain misunderstandings. Beginners tend to focus on surface features, like syntax, rather than deep structure, like iteration.
forEach()
or for...of
for iterating over arrays (with counterpoints)forEach()
and related methods: "Pure functions are easier to reason about than side effects."
b. Counterpoint: But forEach()
accepts callback functions, and functions can indeed have side effects. Pure functions don't have side effects (by definition), but there's nothing to keep a beginner from passing in a function with side effects. And if we use arrow syntax, we're increasing the prerequisite knowledge even more.forEach()
friendlier for beginners, and then this advantage is diminished in that case. In any case, the more verbose option may actually be preferred by beginners (as I mentioned above), so I'd err on the side of for
, since at least it reduces prerequisite knowledge.for...of
loops, since they're less imperative than for
loops (they don't require the user to give direct instructions on how to handle the index).
b. Counterpoint: This a fair point, but intuitively, it seems like less of an obstacle than learning new syntax.forEach()
or for...of
introduces beginners to new language features.
b. Counterpoint: It's true that at a certain point, learners will benefit from this exposure, even if it's just so they can read code written by others. My feeling is that the p5.js reference examples just aren't the right place for this.for
loops for other things, so the syntax won't be deprecated or anything. And this would be a deliberate choice in order to reduce knowledge barriers.forEach()
or for...of
aren't used.
b. Counterpoint: Experienced users will understand the code either way. If we use forEach()
and for...of
, beginners may actually get stuck.Overall, I think a big part of what makes p5.js amazing is that it gives beginners an on-ramp that's not too steep. The p5.js reference is a major piece of that onramp, so making that piece steeper requires a good justification. But I'm open to persuasion.
Edit: Changed the headings for clarity, and removed some redundant language.
I think that there should be different style guidelines for source code vs. examples. Examples should use the most simple and beginner-friendly code, while the library source can use more "cutting edge" stuff.
The point about having the index is interesting. With a
for...of
loop, the index does not come directly but it is possible for the user to usearr.entries()
with a fullfor (const [i, val] of arr.entries())
although in this case the user will need to know about.entries()
and array desctructuring for[i, val]
which it is worth thinking about how it compares with.forEach((val, i) => {})
.
Of those two I have a strong preference for forEach
over for (const [i, val] of arr.entries())
. I think the destructuring is too confusing. But I think that the best approach for example codes is to use a basic for (let i = 0
loop and access arr[i]
within the loop.
On the other hand,
for...of
have a couple possible advantages over.forEach
in that 1. you canbreak
orcontinue
; 2.await
will work as expected.
I very rarely use break
and continue
when I'm writing code. Newer JS array methods like .some()
and .every()
can be used in most cases. IMO we should definitely use these more specific array methods in source code when possible. Probably not in examples though? They are easier to read than an i
loop because it's more like writing a sentence in English. if (arr.some(isWhatever))
is easy to understand. However it would be introducing a beginner to new methods that they may not have seen before.
await
in loops in inherently confusing because there's the distinction of whether you want to run in series or in parallel. no-await-in-loop
is a good eslint rule. Most of the time it should be in parallel and should be await Promise.all(arr.map(doSomethingAsync))
, where we are mapping each element to the Promise
returned by an async function, but that is not at all beginner-friendly.
I don't have a super strong opinion on this one, both
.forEach
orfor..of
have their advantages and I'm happy to use either in examples.One minor bit to add to the discussion: I'm not sure about everyone else, but even though I know the difference between
for..in
andfor..of
, I find it harder sometimes to spot one vs the other when reading code, as both read very similarly in English. I'm not sure if we have any examples that loop over both object keys and arrays, but in that case,Object.keys(array).forEach(...)
is more verbose but maybe clearer to show that something different is happening.
I agree that the difference between for..in
and for..of
is extremely confusing and I think it's too much for a beginner.
- Don't use an
else
block after anif
block that always executes areturn
statement.// Bad. function mouseIsOnLeft() { if (mouseX < width * 0.5) { return true; } else { return false; } }
// Good. function mouseIsOnLeft() { if (mouseX < width * 0.5) { return true; }
return false; }
I disagree with this rule in the current documentation style guide. Since we are aiming for clarity and beginner-friendliness, I think that the explicit else
makes the code easier to read and understand.
For the record, we use the unnecessary else
a lot in the p5.js source code. We have 71 violations of eslint rule no-else-return
if we were to enable that rule.
I really appreciate this discussion! It's great to see a range of perspectives all working to make our documentation that uses iteration more accessible. This a great reminder that there is no universal concept of what is beginner-friendly. As @nickmcintyre said early on
Everyone's learning path is different
To be a bit more open about the origins of my position, let me write a bit about my own learning path: One of my first introductions to code was JS expressions in After Effects. I then moved on to JS for Unity (back when that was a thing), Processing, and Arduino as my first environments for writing programs.
By the time I started using Python, the way it handled iteration confused me. I was so used to a 3 statement for loop, I didn't understand why that was no longer an option.
From Processing Python reference
loadPixels()
for i in range((width*height/2)-width/2):
pixels[i] = pink
updatePixels()
Now reflecting years later, I wished I had first learned to iterate using the Python structure. I find the syntax quite a bit simpler, but more importantly, it removes the risk of infinite loops. I could have had such an easier time experimenting and making mistakes with less severe consequences.
The only reason Python's way of iterating seemed strange and confusing was that I had learned a 3 statement for loop first.
Part of my point here is that I want to question the idea that a 3 statement for loop is the most basic approach. It opens up the risk of infinite loops, and it squeezes 3 statements into 1 line. It feels basic to me personally because I learned it first, but as an educator, I think of it as a hurdle of abstraction and syntax that I need to guide students over to get to the fun creative part and in no way basic.
With all that being said, I've opened issue #6644 for adding a range function that would allow a consistent way for handle numerical iteration and iteration over arrays without the risk of infinite loops.
Thanks @calebfoss! I appreciate all the work and thought you've put into this. These are the kinds of discussions that make p5.js great. To clarify the case I was making, I agree that for...of
is simpler. I also agree that, in general, learning paths vary. However, I'm suggesting that beginners nearly always learn for
loops already, whereas they don't always learn for...of
loops; since for
loops are completely general, it's better to avoid adding something new for them to learn.
It sounds like you're proposing we use for...of
exclusively, so that beginners never need to learn for
loops, which is an interesting idea. My initial concern is that this requires a special p5.js function, whereas looping is a foundational language feature. This means that students venturing outside of p5.js may be misled into thinking that a p5.js range()
function will work in vanilla JavaScript. In the other direction, we may have users coming to p5.js after learning basic JavaScript, and they won't know about p5's special way of doing the numerical loops that you mentioned.
The users who are introduced to loops for the first time with p5's special range()
function will likely be in the minority. You wrote that "I wished I had first learned to iterate using the Python structure"; I can definitely imagine this since I actually learned Python before JavaScript. But for better or worse, JavaScript is the most widely used language. And among those who are introduced to JavaScript, learning the three-statement for
loops is a nearly universal experience; if the JavaScript language itself changes, then I think we'd be having a different conversation.
In general, I feel that p5.js is about opening doors and making creative coding easier in JavaScript, rather than making the underlying JavaScript language easier to work with.[^1] Part of the reason is that JavaScript itself opens a lot of doors. Modifying fundamental language usage also feels out of scope and carries certain risks.
Avoiding infinite loops is an interesting point. This seems like less of an issue with for
loops than while
loops since I don't think beginners usually stray from the basic template with all three expressions (the initialization, the condition, and the afterthought). In their minds, a condition with a <
or <=
operator and an afterthought that increments the variable may as well be required since they've probably never seen anything else. Have you had a different experience with this? I'm curious, but it does seem like an issue we may just have to deal with.
[^1]: It's true that p5.js makes changes like wrapping Math
methods so they're in the global namespace, but that doesn't change the main syntax, and providing user-friendly wrappers for Web APIs doesn't change core JavaScript features. There are also p5.js features like append()
that wrap native array methods, but notice that all but one of these are being deprecated in favor of using the native JS push()
methods. I'm not sure if something similar will happen with string functions, etc.
Avoiding infinite loops is an interesting point. This seems like less of an issue with for loops than while loops since I don't think beginners usually stray from the basic template with all three expressions (the initialization, the condition, and the afterthought).
The case where I normally end up with an infinite loop using only for loops is if I copy-and-paste a for
to do a nested loop. e.g. if I'm trying to iterate in a grid:
for (let x = 0; x < 100; x++) {
for (let y = 0; y < 100; x++) { // Notice it's still x++ here
// ...
}
}
Since I accidentally never modify y
, the inner loop never exits. This may not be a huge issue if the p5 editor ever adds infinite loop detection though.
@GregStanton To contextualize, I teach p5 with university students, many of whom have never written code before. Infinite loops are a very common problem when they are first working with loops. In the p5 editor, it's pretty easy to leave the auto-refresh button on and end up with it running a loop that hasn't been written out completely. I've had the same thing happen in Glitch's live preview window. I make a point of warning students about this in class and in resources I share with them, but inevitably a couple students miss that and sometimes even lose unsaved progress.
I'm under the impression that many p5 users are using the tool to write code for the first time, given its emphasis on beginner-friendliness and use in introductory coding education. It would surprise me if three-statement for loops were a nearly universal experience for folks starting out with p5.
I hear you on not wanting to modify the language. I would just clarify that I'm not proposing modifying the language. For...of loops are a core feature of JavaScript. The range() function I'm proposing is simply a more beginner-friendly way to create an iterator. Similarly, you could write out the arithmetic for linear interpolation in vanilla JS, but the lerp() function makes the process more beginner friendly.
I don't necessarily think our documentation needs to stick with one single structure for iteration, but since I'm hearing that syntax that feels new is less beginner friendly (and that's a big part of my concern about array methods with arrow functions), I'm offering that if we want one consistent way to handle most cases of iteration in our documentation, this would be an option.
Thanks @calebfoss. I hope I'm not being too disagreeable! The approach you proposed is pretty sweet, I think. I'm sort of playing devil's advocate in order to hash out the trade-offs.
To be clear, I'm not saying everyone who starts learning p5 has already seen for
loops. I'm saying that among those introduced to JavaScript, nearly all of them will be introduced to three-statement for
loops. This includes p5.js users, since the current style guide recommends for
loops except in the case of array iteration, and the only kind of loop explained in the p5 reference is the three-statement for
loop (here's the relevant reference page). This means that adding for...of
loops will be an extra thing to learn, unless we get rid of for
loops altogether.
I get that for...of
is a core feature, but you're introducing a new function (range()
) to be used in conjunction with for...of
, in order to accomplish a task that can already be performed within the core feature set. I think this is different than lerp()
and similar features that I mentioned in the footnote to my last comment, since linear interpolation isn't a core feature of programming languages like loops are. Since p5.js is meant to make creative coding easier, it totally makes sense to make such features more beginner friendly.
I guess we can wait to see whether others prefer for
vs. for...of
with the range()
function you proposed. I'm not sure yet where I come down on this; the infinite loop issue is more compelling than I thought. I do think it's a good idea to stick with one option for reference examples if possible.
@GregStanton I think you're bringing up valid points worth considering and not being too disagreeable. Likewise, I hope I'm not coming off stubborn. I want to make a case for my suggestion, but I don't mean to invalidate other options.
I'm actually not sure how I feel about the idea of always using range()
. Making that switch all at once sounds like a big change that could be disorienting for the community and a lot of work to change. I do really like the idea of including it in the library as an option, but I'll focus on documentation in this thread. I think there are significant pros and cons to each option that's been suggested, and I appreciate you all taking the time and energy to discuss.
Yeah, it would definitely be a nice utility regardless of whether it's what we use in the documentation, and a lot of my sketches have a version of a range()
function I keep copy-and-pasting in 😅
So it seems like there are two scenarios we could encounter in the docs: (1) iterations over lists, and (2) iterations that are not backed by data. The range()
function helps turn the second scenario into the first. If we don't use it, that I think means that we would always be using for (let i = 0; i < max; i++) { ... }
syntax for (2) right? And then we still would have to decide on for..of
vs forEach
for (1)? Maybe it's worth trying to decide on how we tackle (1) first, since it seems like a decision we'd have to make regardless of our use of range()
.
The range() function helps turn the second scenario into the first. If we don't use it, that I think means that we would always be using
for (let i = 0; i < max; i++) { ... }
syntax for (2) right?
I think so.
And then we still would have to decide on
for..of
vsforEach
for (1)?
I proposed sticking to for
for this case too, based on the reasoning in this reply. I just realized the headings I included may have made this unclear; I just updated them.
Topic
As we're working on example revisions, I had a few questions about the code style guide.
Tagging @nickmcintyre and @raclim for thoughts