scratchfoundation / scratch-blocks

Scratch Blocks is a library for building creative computing interfaces.
https://scratch.mit.edu/developers
Apache License 2.0
2.6k stars 1.39k forks source link

Allow custom blocks to be shaped like C blocks ("if", "repeat", etc) #1800

Open towerofnix opened 6 years ago

towerofnix commented 6 years ago

(This is a feature request. To my surprise, I couldn't find an issue for this created yet! But maybe I missed one.)

Scratch custom blocks have always been a powerful way of simplifying code. They reduce the amount of duplicate code in a sprite by allowing you to reuse a procedure; text/number and boolean inputs complement this by letting you give specific values to the blocks in the procedure. They even allow for some neat and convenient programming tricks by way of recursion (that is, you can call a custom block from within the definition of that custom block) and "run without screen refresh" (an option that makes the custom block evaluate as quickly as possible, not allowing any other scripts to run at the same time).

Of course, Scratch custom blocks are based on the "procedures" (or "functions") feature found in nearly all programming languages, which also feature inputs/parameters and recursion. However, procedures in many "professional" programming languages allow for a particularly unique idea absent from Scratch: creating custom control structures.


(Click me!) Blathering about text-based languages, so we can compare with Scratch :) (This section is an aside - you don't need to read it to understand the suggestion.) Text-based programming languages are burdened by syntax, which makes creating and using custom control structures a rather daunting practice. For example, start by looking at the following JavaScript function, which prints the numbers from a given start to a given end, and see how it is called: ```js function printNumbers(start, end) { for (let i = start; i <= end; i++) { console.log(i); } } printNumbers(1, 10); ``` This is relatively simple function and is easy for anyone who knows of `for` loops (part of most introductory JS courses) to understand. Now suppose one wants to create a function which can "take behavior as an input"; that is, one where we do something else with the number instead of `console.log`-ing it, and what we do is specified by the code calling `printNumbers` instead of inside `printNumbers` itself. The actual code defining it isn't really complicated, but the way we have to call `printNumbers` afterwords is sort of icky: ```js function useNumbers(start, end, func) { for (let i = start; i <= end; i++) { func(i); } } useNumbers(1, 10, function(number) { console.log(number * 2); }); ``` Take particular note that even though `for` and `useNumbers` are fundamentally similar in that they are both control structures, the built-in `for` loop uses "ordinary JS" syntax that most are familiar with, whereas the user-defined `useNumbers` function requires passing a first-class function. Treating a function as a value is already something new JavaScript programmers are unfamiliar with; passing one into a function and then calling it from inside that function is only more confusing. In Scratch design lingo, this is a "high ceiling, high floor" feature: while custom control structures are a powerful idea, they are difficult to get started with. However, this brings us to custom control structures in Scratch...

Scratch is built around being "high ceiling, low floor", meaning powerful but easy to get started with. And while custom control structures in other programming languages are typically a high-floor feature, Scratch has the advantage of being block-based, so syntax is essentially not an issue. For example, let's look at a simple "show numbers from A to B" custom block:

define "show numbers from (A) to (B)": set Number to A; repeat until Number > B: say Number for 2 secs, change Number by 1

Now suppose we want to make a custom block which lets the user decide what gets done with the number, instead of just "say"-ing it. This is conceptual syntax, but I'll use it to demonstrate:

define "use numbers from (A) to (B) (script)": set Number to A; repeat until Number > B: (the "script" block), change Number by 1

Indeed, writing this was as simple as replacing the "say (Number) for 1 secs" with a block-shaped input.

But how would this look in the block palette (where custom blocks show up)? Well, perhaps it's not actually surprising:

A c-shaped custom block with the label "use numbers from () to ()"

Making use of it would be just as simple; you'd simply use the "Number" variable inside the script:

use numbers from (1) to (10): say (Number) * (Number)

Update December 14: Be sure to take a look at this additional section proposing custom block "outputs", as well as the immediately-below comment with interesting examples of custom C-blocks!

PS, regarding implementation, this would be pretty easy to fit into the UI -- just add another option below "run without screen refresh", something along the lines of "C-shaped". (Probably a better string than that, since it's not particularly self-descriptive to someone who doesn't already know the term. But "run without screen refresh" really isn't that much better - perhaps both ought to have more details that show up by hovering over an adjacent "info" bubble!)

towerofnix commented 6 years ago

PPS, here are some example use cases.

Reusing the custom block we defined above to increment every item in a list:

use numbers from (1) to (length of (my list)): replace item (Number) of (my list) with ((item (Number) of (my list)) + 1)

Repeating until a given number of seconds passes on the timer:

define "repeat until (time) seconds pass (script)": set (start time) to (timer), repeat until (timer - start time) > (time): script

Making use of the suggestion in LLK/scratch-vm#1772 to create a "while" loop:

define "while (condition) (script)": repeat until (not (condition)), script

Implementing the "for" construct in JavaScript, C, etc - an interesting project for a student beginning to learn Scratch with experience in another language, or the other way around:

define "for n = (start value); until n > (end value); increment by (incr) (script)": set (Number) to (start value); repeat until (Number) > (end value): script, change (Number) by (incr)

Using our "use numbers from..." custom block as part of another custom block:

define "for multiples of (mult) from numbers (A) to (B) (script)": use numbers from (A) to (B): if (Number mod mult = 0) then: script

Doing the same thing, but with a user-passed condition (as suggested in LLK/scratch-vm#1772) (plus some demos):

define "for numbers (A) to (B) such that (condition) (script)": use numbers from (A) to (B): if (condition): (script)" --- for numbers (1) to (100) such that (Number mod 10 = 5): say (Number) for (1) secs --- for numbers (1) to (1000) such that (sqrt of (Number) mod 1 = 0): add (Number) to (square numbers)

Of course, there's endless more possibilities, but hopefully these demos should get you started thinking about what you could do with C-shape custom blocks.

joker314 commented 6 years ago

Alternative design (potentially harder to grasp, but more powerful): make the script a type of input, rather than a binary checkbox. The script inputs could be named -- like numeric and string inputs already can be, and the position of the argument wouldn't have to be at the end. This would allow the creation of E-blocks and so on. Example:

If a script takes more than a set amount of time to run, invoke a callback

towerofnix commented 6 years ago

@joker314 I did think about E-shape blocks (etc) while writing this suggestion, but I decided not to include them because they "raise the floor" a lot (so, high-floor instead of low-floor).

PS, neat sketchup. I was thinking of a different syntax though. I don't think your screenshot is very intuitive; instead, I suggest something like this:

define "while <condition> { stack } "

As you can see, the "while" block is an actual C-shape, which is how it will display in the block palette. This matches how the "placeholder block" in current custom blocks match how the block will actually display in the block palette.

mrjacobbloom commented 6 years ago

It'd be awesome if extensions could extend the custom block interface, so callbacks and custom reporters could be opted into by advanced users without adding potentially confusing new features for everyone else, and so (depending on the extension approval/import process or whatever) power-users could just write the extension to add these kinds of advanced features as they need them

Kenny2github commented 6 years ago

I think the most major use case would be: image If this were to be implemented, that definition would appear in every project with any frame complexity.

towerofnix commented 6 years ago

@Kenny2github I wouldn't say that would be the most exciting new use of it (since you could more or less just use a separate custom block for each "run without screen refresh" thing), but it would for sure be helpful, yeah! Thanks for the example.

thisandagain commented 6 years ago

Thanks for filing (and the great overview) @towerofnix! I'm actually surprised this hadn't been filed yet. šŸ˜‚

towerofnix commented 5 years ago

I wanted to bring up an important topic to consider when thinking about custom C-blocks - particularly ones which manipulate variables.

Imagine you have created a simple "count up to (N)" custom C-block using the following script:

define "count up to (N) (script)": set counter to 1; repeat N: (the script block), change counter by 1

Now let's say you place a "count up to (N)" custom C-blocks within another of that same custom block, as in the following example:

count up to (10): count up to (5): (empty)

The question is.. how do you access the two counters separately? - For example, to multiply them together and "say" the result. There isn't any obvious way! Worse off, this script above is already "broken"; look at what would happen if you did this:

count up to 4: say (join "Outside: " counter), count up to 3: say (join "Inside: " counter)

Outside: 1
Inside: 1
Inside: 2
Inside: 3

Outside: 4
Inside: 1
Inside: 2
Inside: 3

Outside: 4
Inside: 1
Inside: 2
Inside: 3

Outside: 4
Inside: 1
Inside: 2
Inside: 3

Can you see what's happening? The inner loop is manipulating the counter variable - which is also used by the outer loop!

(Just to take a step back, "Why is this a problem?" Well, users will definitely try sticking a custom C-block inside another of the same custom C-block, which is quite a handy action in general. Plus, this type of problem could come up in other situations too, like two threads (scripts) running the "count up to (X)" block at the same time -- they'd both be manipulating the same variable, and this would cause a similar problem as we see above.)

So, what's the solution? Perhaps an idea of "outputs". You would add an output to your custom block the same way you'd add a text/number input, a boolean input, or a label - it would just be another option. Then you change its value inside your C-block like any variable. Once we add one to our custom C-block's code, it might look like the following:

define "count up to (N) (counter) (script)": set counter to 1; repeat N: (the script block), change counter by 1

And the block in the palette (and when dragged into the scripting area) would look like this:

The empty "count up to (10)" C-block, with a "counter" variable after "(10)"

The "counter" variable could actually be dragged out, as though it were coming from the palette - it would create a copy of that block, to be used inside the script. For example:

count up to (10) (counter): say (counter)

Now if you dragged another "count up to" block and dropped it inside that existing "count up to" block, it would automatically rename the output block you see from "counter" to "counter 2":

count up to (10) (counter): count up to (5) (counter 2): say (counter) * (counter 2) for 1 secs

(You could also right-click and rename the variable just like you can with any other variable - it would change all uses of, for example, the "counter 2" variable within that same script/C-block, to the new name you want.)

This idea of custom block outputs can be explored in a lot of different ways to make it as intuitive and useful as possible, but I've explained the gist of the idea and how it can be used to make custom C-blocks realistically useful by avoiding the problems described above.

joker314 commented 5 years ago

Is 'counter' available in the variable drop-down menu of the 'set variable' block even if that block is not inside of the C-block yet? If not, it might make the output variables less intuitive (some users may look through the drop-down menu of the 'set variable' block in the palette before dragging it out)? But if yes, then it's easy to make a mistake and set a variable not in your 'scope' (would this actually change the variable, or be ignored?).

If they actually do set the (out of scope... or not! variable), then two C-blocks can't have the same names as each other or it'd be ambiguous.

Can the output variables be monitored on the stage, perhaps via right click?

ihadastrok commented 2 years ago

How tf should I replace say (number) for 1 secs to that block shaped input!?

jessiejs commented 1 year ago

This is a very well-thought-out concept, there probably shouldn't be a "is c shaped?" tickbox, but rather just alongside the rest of the input types, you have the output, and an option to add a C.

SimonShiki commented 1 year ago

A prototype of this functionality I tried to implement in my scratch fork (preview). I would like to share some of my thoughts on this idea.

  1. now the value of arguments can only be correctly obtained when defining a custom block. But this idea may change that and may create difficulties in understanding. For example: Maybe we need to consider that the C-shape block is only used to control the running flow.
  2. The following is a common usage of custom block with branch: actually, it won't work because condition won't be re-evaluated.
  3. unlike Scratch 2, argument's color is same as procedures call. This may cause some confusion.

Regardless, I would love to see this feature officially added to Scratch.

towerofnix commented 1 year ago

actually, it won't work because condition won't be re-evaluated.

This is an important concern. In C blocks we could have inputs automatically "re-evaluate" each time they are used (if the reporter is non-pure, e.g. including a custom reporter) or have their values de-cached each time the stack-shaped input is evaluated (since those definitely aren't pure, by definition).

Snap! goes the extra mile and allows you to control exactly when inputs are evaluated, and which inputs are automatically evaluated as soon as you run a block (the default) and which wait for evaluation (with a "call" or "run" reporter block). This is obviously excellent behavior that I'd love to see at least similarly in Scratch, but it goes moreso with first-class code as a whole.

SimonShiki commented 1 year ago

actually, it won't work because condition won't be re-evaluated.

This is an important concern. In C blocks we could have inputs automatically "re-evaluate" each time they are used (if the reporter is non-pure, e.g. including a custom reporter) or have their values de-cached each time the stack-shaped input is evaluated (since those definitely aren't pure, by definition).

Snap! goes the extra mile and allows you to control exactly when inputs are evaluated, and which inputs are automatically evaluated as soon as you run a block (the default) and which wait for evaluation (with a "call" or "run" reporter block). This is obviously excellent behavior that I'd love to see at least similarly in Scratch, but it goes moreso with first-class code as a whole.

The reason for this issue is complicated, because the logic behind it is completely correct, but it is counter-intuitive.

Scratch executes each block from inside to outside, from left to right. The reporter's value passed in should be kept. What the "repeat until" block does is not just recompute the condition passed in, but prevent jumping to the next block if the condition is not met. (util.startBranch(1/* branchNum*/, true/*isLoop*/)). (I guess it is to distinguish this logical difference that there will be a "return" icon in the lower right corner of each block that may be executed repeatedly)

But this problem is not easy to handle for custom block with branch, because the above logic involves understanding the execution logic of Scratch, which also troubles Scratchers. What's more, this may have deviated from Scratch's easy-to-understand design concept.

towerofnix commented 1 year ago

Oh, that feels far out of the scope of any Scratcher to concern themselves with. It's good wisdom from someone who's gone to actually implement this feature, of course. But no Scratcher should be worried that loop-like blocks really get run multiple times, in the context of an (externally stored!) external "memory" that the loop block has to both prepare state for and restore state from. It's an intriguing way of designing functions, but useless for making users get acquainted with "functions that are provided callbacks" in the first place - that's just not how code works in contexts outside of Scratch, as far as I'm aware.

towerofnix commented 1 year ago

now the value of arguments can only be correctly obtained when defining a custom block. But this idea may change that and may create difficulties in understanding.

I'd encourage you to take a look at the earlier comment about "outputs" for custom blocks https://github.com/scratchfoundation/scratch-blocks/issues/1800#issuecomment-447386793 which is inspired by "upvars" in Snap!. It's basically the same idea, but I think presented in an easier to understand way. You drag the reporter block out of the instance of the custom block (very close to where you're writing its sub-script!), not its definition (generally far away).

I used "set variable" blocks in my examples. That's what Snap! does, but you could also imagine a presentation like:

But this has much more limited applicability because it doesn't create a variable that's actually locally scoped to the custom block - it would run into issues if you put a "count up to" block inside another "count up to", because both of them would be accessing the same "counter" variable defined on the sprite (or project), rather than each its own unique "counter" variable.

I haven't worked with Scratch's execution engine in a long time so wouldn't know exactly how to go about implementing this, but expect it would involve changing the get/set variable blocks to pull from the execution context of the custom block.

Also, I want to mention that this could be used as a rudimentary way of passing values back and forth, along the lines of a custom reporter. For example:

See how we change the variable outside the custom block's definition, within the scope of its sub-stack, instead?

Of course, this isn't going to work very nicely with the existing "count up to" script. It repeats however many times it was inputted, and then stops. It might count up to 40, then we interrupt it, then it counts up to 60... and quits! Since we want to let it be able to restart - if we tell it to, from within the sub-stack - we really want a definitionĀ more like this:

Cool! You can imagine there are more interesting uses than just altering a loop midway through, but this already shows something custom C-blocks are good for.

(I'm a big, big sucker for script-local variables, so I think that "outputs" should work for stack-shaped custom blocks too, i.e. really the "output" lexically exists in later blocks in the same script, too, not just ones that are nested under a C-shaped custom block. But that's less critically important than even basic support of available-when-nested outputs in C blocks.)

ayreesa commented 4 months ago

when gf clicked move (15) steps they can also be cap blocks. Screenshot 2024-07-07 232324

ayreesa commented 4 months ago

when stop sign clicked :: events :: hat

ayreesa commented 4 months ago

`when gf clicked

ayreesa commented 4 months ago

`forever if <>

end`

ayreesa commented 4 months ago

`forever

end`

ayreesa commented 4 months ago

`forever

end`

ayreesa commented 4 months ago
when gf clicked
forever
     if on edge, bounce
end