Shadowing variables in nested loops

FWIW, I guess I mostly disagree with all of you :smirk: … what happened to the spirit of “a broadly inviting language for kids and adults” with “wide walls” and a “low floor”?

Of course the dev team is not ”obliged” to imagine anything, but if it’s easy to do so, why not? Users (such as kids) may be inexperienced, but that doesn’t make them “fools”. And no one should be “punished” for playfully trying their hand at something they don’t (yet) master at all.

I don't think it's about imagining anything. The OP here is to make special provisions for a special use case, that of nesting two FOR loops with a same named iterator variable, and then to automatically change the name of one of them. Aside from the many questions arising out of that (e.g. what if one FOR loop was added by wrapping it around an existing part of a script that already contains references to an existing iterator variable, should we also rename all the references, or wouldn't it be okay to allow such a name collision while the user is constructing some bigger idea instead of over-eagerly interfering with them?). There might be perfectly valid reasons to have same-named conflicting variables at times, and be it temporarily while you're building up to something else, or while you're translating a project to another spoken language (as happens with me a lot, when I'm taking a project to localize it to German).

I don't believe in "punishing" users at all! But being "broadly inviting" doesn't mean to second-guess the user's intention and to reformulate their utterances, it's to support them to find their own voice to express what they wish. Remember that the biggest atrocities during the French Revolution originated from - of all - the Welfare Committee.

I don’t support the OP’s suggested solution either. What I’ve been proposing is to limit variable scope.

That’s Godwin-in-disguise … you lose :frowning:

But that's just saying something without thinking about all the consequences! Limiting upvars to C-Slots would do just that, and would make it impossible to use upvars in any other context, i.e. without a C-Slot. So, of course, you're going to claim that somehow we should let the user specify which slot - if any - they want to limit the upvar scope to. But you're probably not seriously expecting such semantic intricacies to be any less confusing, let alone the UI to such options to be any clearer. C'mon on!

An iterator to be used only within, and so as to control, a loop (esp. for) is an important use case of what is called an upvar in Snap! This use case is employed by beginners (often without awareness of its nature), and advanced users alike. IME reusing the iterator outside of the loop is usually erroneous or (ahem …) bad practice; so is in-loop code changing the iterator.
Why not create a special type for this purpose, perhaps called … iterator? An iterator will only be changed by the control loop block definition code itself, and will be valid only within the control loop. If a variable bearing the same name exists outside the loop it will be considered a separate entity.

Other use cases of present-day Snap! upvars are e.g. let-like constructs, secondary output of a function, or whatever. Mostly employed by advanced users. The scope of these upvars may remain as-is, or perhaps be limited to the first nesting level outside the loop.

Thirdly I've been wondering why Untitled script pic (88) couldn't be truly local, i.e. valid only within the nest level they're created in (and below) - "because they're called script variables, you Dummkopf" :wink:. Seriously, if they need to be visible within e.g. the entire script, the user may create them at that level. All of this is like Scheme, which your co/foe-creator of Snap! must like :slight_smile: .

I'm not asking anyone to make any changes at once - it's just that I put it up for consideration one day in the future.

In fairness to the OP, all he asked for was a warning about overlapping scopes for the same name. We haven't so far gone in for warnings in situations in which we think the user can't have meant what they said, and I don't think we're obliged to follow this suggestion, but it's a very reasonable request. All this stuff about changing the scope of variables has been proposed by third parties, such as me.

I think the reason this is such a hot-button topic is the initial decision to treat an upvar as following the same rules as a script variable. The two notations are very different; there's no UPVARS block you can include in a script the way you can SCRIPT VARIABLES. Instead you have to associate the upvar with a block, whether it be a primitive block such as FOR or WHEN ANY KEY PRESSED, or a custom block.

When the block in question is a hat block, it's natural to expect that the scope of the variable is the entire script under that hat. I guess the same is true about the BLOCK variable in the DEFINE block, although I have to admit that that's always felt to me like a fragile solution, in which the properties of a block can be set only at the same time that it's created, and a defined block can call itself recursively only with an explicit CALL. (I haven't complained because I don't have a better proposal.)

But in the cases in which the upvar is part of a C-shaped block, as in FOR and FOR EACH ITEM, I don't think it's crazy for a user to expect the variable to have a scope limited to the block itself. I point out that grownup languages differ among themselves on this question.

I'm not suggesting that users should nest loops with the same index variable! Nor that we should encourage them to do so. And I point out that my proposal (as opposed to qw23's) doesn't allow a variable that's been shadowed to "reappear" at the end of a loop.

(Although, ya know, let's say a user has a global variable called ITEM, and uses a FOR EACH ITEM block inside a script without thinking about it very hard, and then expects to be able to use the global ITEM after the loop is done. That's not as self-evidently loopy, to coin a phrase, as the example with nested FORs. I have some sympathy for that user.)

But, forget about upvars for a moment, and forget about loops altogether. Suppose a user does this:

untitled script pic (3)

DId this user mean something comprehensible, and what should we do when we see the second declaration? One possible answer is just to ignore the second declaration on the grounds that the variable already exists. But another possible answer is to make a new variable shadowing the old one for the remainder of the script.

What difference would that make? If the scope of the new FOO is the entire part of the script following the second declaration, how does it help anything to keep around a previous variable with the same name, no longer accessible?

Answer: It helps if, in the part of the script between the two declarations, the user has created (by ringification) a procedure that captures the first version of the variable. That procedure may remain available after the second declaration, and indeed after the script is completely finished if it's a reporter that reports the procedure.

(Side comment: After 11pm my computer hides all my windows and makes me say "give me another 15 minutes" to continue work. Hence I realize that I've been writing this post for over an hour now... sigh...)

If, instead of a plain script variable, the reused name is in the context of an upvar, that doesn't fundamentally change anything, but it does mean that the variable has two names in two different environments, and so it's that much more likely that the same variable may be accessible under a different name in that other environment.

So, to summarize, I think it wouldn't break any existing code and would be useful if creating a second variable with the same name in the same script actually did create a second variable and preserve the first one.

There is no such thing as a C-shaped block, Snap lets you specify individual input slots to be rings, sometimes in the guise of being C-shaped. There is absolutely no rule, not even a best practice that postulates an upvar to have any kind of connection to a particular input slot, let alone be limited to the scope of a particular input slot.

I absolutely understand that there is expressive beauty and power in specifying formal parameters (in the form of explicit variable blobs) for lambda constructs. Snap! has that in the form of ring inputs. C-shaped slots are an abstraction of (full rings), they do not feature explicit inputs. What you want is to be able to specify formal parameters for C-slots. I understand that, and you've got a case for that. But I disagree about adding that to Snap.

Abstraction in UI design is something we'll forever disagree on. For you, there isn't any, because you want to be able to do everything in every situation all the time, whereas I believe in limiting the scope of features in the interest of welcoming novices.

Some two cents from me :slight_smile:

Programming languages differ from each other in many ways

The loop variable "leaking" from the loop can be a surprise when you first come across it

Once you've understood that Snap! says any variable declared in a script lasts until the end of script, then you just accept that and move on.

In practice, it's not a real issue

There are simple workarounds if needed due to conflicts with global vars

untitled script pic (39)

What makes me both sad and mad is that we've implemented this - I believe - ravishingly beautiful and unique feature that visualizes variable scope live in your editor, something not found in any other programming language I'm aware of, and yet here, in this very forum, y'all are once again nitpicking about peculiar special cases and seemingly "advanced" uses of same-named variable declarations inside nested scopes for absurd side effect tricks like insisting that you can create your own syntax for lambda calculus by repurposing existing Snap syntax elements that have been created for entirely different purposes, and claiming this to be a desirable goal of an educational programming environment. How can I not despair?!

That is a great feature indeed!

Sadly, hardly anyone knows about it, I suppose (I, for one, didn't - before you mentioned it within this forum topic). It’s not mentioned in the Reference Manual; it's not highlighted in BJC either (Unit 1, Lab 3, visible stepping) ... Seriously, documenting and communicating existing great features may require more attention at this point than developing new ones.

Yeah, you have a point here. IMO design decisions should be geared at minimizing risk of beginners getting confused, while maintaining flexibility for advanced users. So I think our goals are alike - we may just not always agree on how to get there.

See it form the bright side. Overall the Snap! team are doing a great job. Nitpicking, like "first-world problems", can only exist in a very well developed environment.

the issue

what is the issue?

aww, thank you, @qw23

Yes, we seriously have to think about how to better document features that have been added or refined since v8.

Lambda scoped upvars can be emulated with the ring formal parameters
untitled script pic - 2024-09-03T201146.437
May require some tweaking of the target context...

untitled script pic - 2024-09-03T201442.804

untitled script pic - 2024-09-03T201704.284

Thank you! Metaprogramming to the rescue. This may be one fewer thing I have to keep arguing with Jens about! :~) :~) :~) <3


Aww I'm sorry for making you despair. But that does seem like an extreme response to what I still consider a modest proposal. It's something a beginner would never notice, since it wouldn't change the behavior unless you capture the variable in a closure between the two declarations.

The feature about showing the scope of variables is indeed beautiful. I wasn't aware of it until now either!


Changing the subject from this particular proposal to the general question of what's desirable in an educational programming environment:

I think this is a harder question than you imply here. Take hyperblocks, for example. I am deliberately picking a feature you know I love as an example! But it's something beginners can trip over, because they make domain errors and get an answer instead of an error message. They're writing a program about lists of numbers, and some subprocedure expects a number as input, and they accidentally call it with a list as input. Students do that all the time; for example, it's what the mobile problem in SICP is largely about:

Exercise 2.29. A binary mobile consists of two branches, a left branch and a right branch. Each branch is a rod of a certain length, from which hangs either a weight or another binary mobile. We can represent a binary mobile using compound data by constructing it from two branches (for example, using list):

(define (make-mobile left right)
(list left right))

A branch is constructed from a length (which must be a number) together with a structure, which may be either a number (representing a simple weight) or another mobile:

(define (make-branch length structure)
(list length structure))

a. Write the corresponding selectors left-branch and right-branch, which return the branches of a mobile, and branch-length and branch-structure, which return the components of a branch.

b. Using your selectors, define a procedure total-weight that returns the total weight of a mobile.

c. A mobile is said to be balanced if the torque applied by its top-left branch is equal to that applied by its top-right branch (that is, if the length of the left rod multiplied by the weight hanging from that rod is equal to the corresponding product for the right side) and if each of the submobiles hanging off its branches is balanced. Design a predicate that tests whether a binary mobile is balanced.

d. Suppose we change the representation of mobiles so that the constructors are

(define (make-mobile left right)
(cons left right))
(define (make-branch length structure)
(cons length structure))

How much do you need to change your programs to convert to the new representation?

So, my point is, instead of just using the needs of beginners as a fetish to cut off discussion, it's important to ask how likely each particular feature is to get in their way, versus how much the feature expands what's possible.

My particular feature would expand what's possible only to a small extent, which is why you can refer to it as "nitpicking," but it also, I claim, has no actual downside for learners.

As for "creat[ing] your own syntax," the ability to do that is what separates the great languages from the plebian ones. That's why metaprogramming, which is of no direct benefit to beginners, is so important in Snap!. It's a feature for advanced users, precisely so they can create their own syntax.


Of course you know I know this. Nevertheless, you also know that for users, and especially for beginners, it is the block that's C-shaped, and that's a super important abstraction. In Scratch everyone talks about C-shaped blocks, not C-shaped slots. They even talk about "E-shaped blocks" such as IF/ELSE. I hope that, as a teacher, you wouldn't rush in to correct a student who referred to a "C-shaped block." It's this abstraction that enables students to get from
untitled script pic (4)
to
untitled script pic (5)
in their understanding of loops.

This is another instance of what I see as our most fundamental recurring disagreement: You insist that users think in terms of how the language is implemented, rather than in terms of its behavior.


Thank you, this feels very different to me from the "foolish" and "absurd" that has heretofore characterized your responses. I appreciate it.

Thank you all for the discussion!

Actually I take back my proposals.

The 'warning' proposition came from my experiense with IntellijIDEA, which is designed more about performance (warning you about anything) rather than learning.

I was thinking that if I did that 'mistake' (nesting two for loops with the same variable) then somebody else might do it and we should try to protect them. But I don't want to protect somebody from making that mistake, then debugging it, then posting a question to the forum and learning that much (e.g debugging variables' scope, metaprogramming to the resque, etc). So again - thank you for sharing.

I thought I was worried about giving a public demo with a broken project. But debugging live in front of the audience, then discussing variables scopes and who-know-what-else, might be even better than the discussion here.

@jens It seems that you consider 'foolish', users who drop 2 for-loops (one inside the other) and expect 100 iteration. On the other hand it seems that you care about welcoming novices.

I do believe that it's more welcoming to novices if a programming language does what the programmer tells it to do rather than to automatically "correct" them and silently do what the implementors of the programming language guess the user wants. Telling somebody in their face that they don't really mean what they say can be quite paternalizing and condescending. I'd much rather make an environment that lets novices make mistakes without punishing them with unrecoverable system crashes and even lets them go back and examine what's happening and why. The idea of Snap! isn't to not let you make mistakes but to invite you to experiment, explore, discover and debug. Debugging is a foundational part of the constructionist pedagogy, it's where learning happens.

With respect to nested loops with the same variable name, I don't think anyone is seriously proposing to "silently do" something other than what the program says; that's a straw person argument. The other option besides silently letting the program fail and letting the user debug it is to give a thoughtful error message when we can discover an error before actually running the code.

I mean, yes, the experience of having a bug in your code and finding it and fixing it is very valuable; the idea that something can be debugged is arguably the most important reason programming belongs in education. It contrasts sharply with the more common school experience of just getting a bad grade because you made a mistake! But I also believe that no matter how hard we try, we won't prevent kids having that experience. :~) It's like a quantum uncertainty principle in the domain of code. Any significant programming project will have bugs.

I'm so much in agreement with your principle about debugging that when we were writing BJC, in the lab about fractal trees, I originally wrote it so that a lab page ends with kids constructing a recursive procedure with no base case and discovering the infinite loop, not addressed until the next page, i.e., the next class session. I thought it would be good to live with that bug overnight! But I was overruled by the EDC people, whose greater experience writing high school curriculum taught them not to do that. The solution had to happen the same day as the problem.

But also, in the context of non-nested redeclaration of a variable name, it's possible for language designers to change the meaning of what some piece of code "tells it to do." For example, during the first, what, seven years? of BYOB/Snap!, it didn't mean anything for a user to add a number to a list of numbers. Then we decided it would mean an APL-style termwise addition. We could, instead, like some other language designers, have decided to overload the + block so that number+list would mean IN FRONT OF, and list+list would mean APPEND. Or we could have decided that either of those language choices would have been illegitimately trying to read the user's mind, and kept it as a domain error, keeping the process of iterating over a list more explicitly visible in users' code.

Similarly (and I'm not proposing this, it's just a thought experiment), we might decide that if an atomic value is considered equivalent to a list of length one in the context of arithmetic, we should extend that idea so that MAPping a function over an atom would be treated as if it were CALL instead of MAP, rather than giving an error message. As a language designer, I'm not inclined to make that choice, but if some other language did it, I wouldn't be offended. It wouldn't, imho, be as bad as overloading + to mean APPEND, or JOIN, another thing some languages do.

That is, it's not a question of guessing what the user meant; it's a question of settling on a meaning for something whose meaning would otherwise be unclear. It's just like language designers having to decide what IF A THEN IF B THEN C ELSE D means. It's not us guessing what the user means; it's us telling the user what we mean by some construction! We're allowed to do that.

No matter what anybody may think of it, I tried my hand at rebuilding the FOR loop block such that the iterator is local.

It has been a mixed success so far:

Thie next script more or less works: it says (1 2 3 2 3), but i is affected.

local for iterator script pic 3

However the following script does notwork:

It can be fixed, but that’s very complicated and I’m not sure if it might be generalized:

Mutating a ring expression with formal parameters the same as upvar, does not work for you?
untitled script pic (3)

local variables script pic (5)

With the new V10 variadic ...
local variables script pic (6)

It shouldn't have to be that complicated. Imho.