Bug #20930
openDifferent semantics for nested `it` and `_1`
Description
With --parser=parse.y:
$ ruby --parser=parse.y -ve '[1].each { p it; [5].each { p it } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) [x86_64-linux]
1
5
$ ruby --parser=parse.y -ve '[1].each { p _1; [5].each { p _1 } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) [x86_64-linux]
-e:1: numbered parameter is already used in
-e:1: outer block here
[1].each { p _1; [5].each { p _1 } }
ruby: compile error (SyntaxError)
The behavior is inconsistent between it and _1.
Side note about mixing `_1` and `it`, which seems good
As an aside, mixing _1
and it
is allowed, I think this is good, they are different things so there is not much confusion there:
$ ruby -ve '[1].each { p _1; [5].each { p it } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) +PRISM [x86_64-linux]
1
5
$ ruby -ve '[1].each { p it; [5].each { p _1 } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) +PRISM [x86_64-linux]
1
5
Prism's bug, moved to: https://github.com/ruby/prism/issues/3291
$ ruby -ve '[1].each { p it; [5].each { p it } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) +PRISM [x86_64-linux]
1
5
$ ruby -ve '[1].each { p _1; [5].each { p _1 } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) +PRISM [x86_64-linux]
1
1
Notice the inconsistency, it
uses the innermost block, _1
uses the outermost block.
I think _1
semantics are slightly better, at least _1
behaves like a normal local variable declared in the outer block then.
Note that on 3.3.5 it was forbidden to nest _1
which I think might be good for clarity/avoiding ambiguity:
$ ruby -ve '[1].each { p _1; [5].each { p _1 } }'
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-linux]
-e:1: numbered parameter is already used in
-e:1: outer block here
[1].each { p _1; [5].each { p _1 } }
ruby: compile error (SyntaxError)
Updated by Eregon (Benoit Daloze) 14 days ago
With --parser=parse.y
:
$ ruby --parser=parse.y -ve '[1].each { p it; [5].each { p it } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) [x86_64-linux]
1
5
$ ruby --parser=parse.y -ve '[1].each { p _1; [5].each { p _1 } }'
ruby 3.4.0dev (2024-12-04T19:29:24Z master 3c91a1e5fd) [x86_64-linux]
-e:1: numbered parameter is already used in
-e:1: outer block here
[1].each { p _1; [5].each { p _1 } }
ruby: compile error (SyntaxError)
Which is still inconsistent between it
and _1
.
Updated by Eregon (Benoit Daloze) 14 days ago
- Related to Feature #18980: Re-reconsider numbered parameters: `it` as a default block parameter added
Updated by mame (Yusuke Endoh) 14 days ago · Edited
Good catch. I see two problems.
One is an incompatibility with Prism's handling of _1
. I think it should be handled as an error like parse.y. Especially when parsed with the 3.3 version's syntax, there is no other choice but an error:
Prism.parse("[1].each { _1; [2].each { _1 } }", version: "3.3.0")
The other problem is how to interpret it
. I think Ruby master's it behavior is good. I have experienced mistakes of a name conflict issue of a local variable in a different scope, but not as often. However, just adding a read from it
outside of a block only changes the meaning of inner it
, which I think would increase the frequency of mistakes very much.
[1].each { [2].each { p it }} #=> 2
[1].each { it; [2].each { p it }} #=> 1 (!)
Updated by k0kubun (Takashi Kokubun) 13 days ago
I think _1 semantics are slightly better, at least _1 behaves like a normal local variable declared in the outer block then.
Yeah but _1
isn't declared in the outer block (or anywhere), so _1
doesn't necessarily need to behave like a local variable declared in the outer block. You could also say "_1 should behave like a normal local variable declared in the inner block", and it seems as plausible as what you said.
The other problem is how to interpret it. I think Ruby master's it behavior is good. I have experienced mistakes of a name conflict issue of a local variable in a different scope, but not as often. However, just adding a read from it outside of a block only changes the meaning of inner it, which I think would increase the frequency of mistakes very much.
I agree with @mame (Yusuke Endoh) 's opinion.
When you have nested loops, you would use the iterator of the inner-most block most often. The current behavior of it
seems to have more use cases than the one of _1
. Ruby has prioritized solving real-world use cases over just making existing features consistent, so I don't think it
needs to be consistent with _1
here.
Updated by Eregon (Benoit Daloze) 12 days ago
I think it's OK for it
to always use the innermost block, but it should be a conscious choice and ideally documented (maybe even part of NEWS
).
If we go there, I do think we should use the same semantics for _1
, _2
, etc for consistency, because they are extremely similar constructs. Otherwise it
would become more powerful than _1
but they should really just be equivalent.
Updated by k0kubun (Takashi Kokubun) 9 days ago · Edited
- Description updated (diff)
Let me get this straight. _1
was introduced long before Prism was merged. Prism allowing _1
in different levels of nested blocks is just a bug of Prism (https://github.com/ruby/prism/issues/3291), so let's not talk about that here. I updated your issue description to clarify what's actually inconsistent between _1
and it
.
As to nested _1
being a SyntaxError
and nested it
being allowed, the behavior of it
looks good as is because it seems useful to be able to nest blocks like files.each { YAML.parse_file(it).each { p it } }
and it doesn't seem confusing to me. Since this particular case was not discussed before, I'll put this ticket into the dev-meeting agenda to confirm that we all intend that.
Updated by mame (Yusuke Endoh) 6 days ago
Discussed at the dev meeting, and matz confirmed that the current master's behavior is good.