Bug #20930
closedDifferent 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) 10 months 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) 10 months ago
- Related to Feature #18980: `it` as a default block parameter added
Updated by mame (Yusuke Endoh) 10 months 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) 10 months 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) 10 months 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) 10 months 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) 10 months ago
Discussed at the dev meeting, and matz confirmed that the current master's behavior is good.
Updated by k0kubun (Takashi Kokubun) 10 months ago
- Status changed from Open to Rejected