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

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

Open
towerofnix opened this Issue Nov 30, 2018 · 9 comments

Comments

Projects
None yet
5 participants
@towerofnix
Copy link
Contributor

commented Nov 30, 2018

(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:

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:

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

This comment has been minimized.

Copy link
Contributor Author

commented Nov 30, 2018

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

This comment has been minimized.

Copy link
Contributor

commented Nov 30, 2018

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

This comment has been minimized.

Copy link
Contributor Author

commented Nov 30, 2018

@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

This comment has been minimized.

Copy link
Contributor

commented Nov 30, 2018

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

This comment has been minimized.

Copy link
Contributor

commented Dec 2, 2018

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

This comment has been minimized.

Copy link
Contributor Author

commented Dec 2, 2018

@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 thisandagain added this to the Backlog milestone Dec 3, 2018

@thisandagain

This comment has been minimized.

Copy link
Member

commented Dec 3, 2018

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

@towerofnix

This comment has been minimized.

Copy link
Contributor Author

commented Dec 14, 2018

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

This comment has been minimized.

Copy link
Contributor

commented Dec 14, 2018

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.