Feature #19370
closedAnonymous parameters for blocks?
Description
Just to clarify: are anonymous parameters delegation is planned to support in blocks?
It would be a nice addition, if it is possible to implement:
# data in form [request method, URL, params]:
[
[:get, 'https://google.com', {q: 'Ruby'}, {'User-Argent': 'Google-Chrome'}],
[:post, 'https://gist.github.com', 'body'],
# ...
].each { |method, *| request(method.to_s.upcase, *) }
...and at the very least, consistent with what the method definition can have.
If they are NOT planned to be implemented, I believe that at least error messages should be made much clearer, because currently, this would happen while running the code above:
no anonymous rest parameter (SyntaxError)
I understand the reason (the request
clause doesn't "see" anonymous parameter of the block, and claims that current method doesn't have them), but it looks honestly confusing and inconsistent.
Updated by nobu (Nobuyoshi Nakada) almost 2 years ago
- Status changed from Open to Assigned
- Assignee set to matz (Yukihiro Matsumoto)
IIRC, matz was negative against it.
Updated by zverok (Victor Shepelev) almost 2 years ago
Note that it might lead to very confusing behavior when the surrounding method does have anonymous arguments:
def test(*)
# ...
proc { |*| p(*) }.call(1)
end
test(2) #=> prints 2 (method's anonymous arguments, not proc's)
Note that code is valid, and sanely looking syntax, which is very easy to (mis)interpret: it is "obvious" from the first sight that proc uses its anonymous args.
The resulting behavior might be very confusing (especially in complicated code, long methods, and subtle differences in data).
Updated by Dan0042 (Daniel DeLorme) over 1 year ago
zverok (Victor Shepelev) wrote:
If they are NOT planned to be implemented, I believe that at least error messages should be made much clearer, because currently, this would happen while running the code above:
no anonymous rest parameter (SyntaxError)
Perhaps this message would be clearer: "no anonymous rest parameter for method `xyz' (SyntaxError)"
Updated by sawa (Tsuyoshi Sawada) over 1 year ago
zverok (Victor Shepelev) wrote in #note-2:
Note that it might lead to very confusing behavior when the surrounding method does have anonymous arguments:
def test(*) # ... proc { |*| p(*) }.call(1) end
I think nested anonymous arguments should be prohibited as with numbered parameters:
"foo".then{_1.then{_1.upcase}}
# >> SyntaxError ((irb):21: numbered parameter is already used in) outer block here
Updated by Dan0042 (Daniel DeLorme) over 1 year ago
sawa (Tsuyoshi Sawada) wrote in #note-4:
I think nested anonymous arguments should be prohibited as with numbered parameters:
That's a good idea, but does it mean this would become valid?
def test
proc { |*| p(*) }.call(1)
end
Updated by Eregon (Benoit Daloze) almost 1 year ago
- Related to Bug #19983: Nested `*` seems incorrect added
Updated by zverok (Victor Shepelev) almost 1 year ago
After considering some examples, I believe it would be more consistent to just allow anonymous parameters in blocks, without further restrictions.
The example of "confusing" example above:
def test(*)
# ...
proc { |*| p(*) }.call(1) # which * is that?
end
...can be rewritten with named arguments as well:
def test(x)
# ...
proc { |x| p(x) }.call(1) # which x is that?
end
In any case, the current behavior is the most confusing, as demonstrated by #19983
Updated by mame (Yusuke Endoh) 12 months ago
We discussed this issue and #19983 at the dev meeting.
- Only method arguments can be delegated by
foo(*)
. - It should raise a SyntaxError to use
foo(*)
in a block that accepts*
explicitly. - It is allowed to use
foo(*)
in a block that does not accept*
explicitly.
def m(*)
->(*) { p(*) } # SyntaxError
->(x, *) { p(*) } # SyntaxError
->(x) { p(*) } #=> 1
proc {|x| p(*) } #=> 1
end
m(1).call(2)
The main discussion was as follows.
-
def m(*); ->(*) { p(*) }; end
is indeed confusing. - We want to prohibit a case where takes
*
and**
from different blocks, such as->(&) { ->(**) { ->(*) { foo(*, **, &) } }
. - We want to allow
def m(*) = @mutex.synchronize { m2(*) }
enclosed in a block that does not accept*
. So we cannot prohibit to take arguments from outer blocks (or the method arguments). - Should we allow to take block arguments in unambiguous cases, such as
def m; ->(*) { p(*) }; end
?- @knu (Akinori MUSHA): In many cases, what we want to delegate is the method arguments.
- @ko1 (Koichi Sasada): Once we prohibit it, we can allow the delegation of block arguments later, if it is really needed.
Updated by zverok (Victor Shepelev) 11 months ago
To be completely honest, I think I have some counterarguments to the current conclusions.
I believe the code that looks "logically possible" should be "physically possible" (unless there is a good argument why it is impossible, like the ambiguity of parsing).
In the case of allowing forwarding in blocks, I find two questions below (with their answers) to be important.
Would this create new types of confusion?
I believe it would not.
-
def m(*); ->(*) { p(*) };
, if it would forward the proc args MIGHT BE considered confusing. But it is confusing in EXACTLY the same way asdef m(a); ->(a) { p(a) };
; no new complication to explain here. Such cases with named variables are easily caught by the linter if the user wants (and doesn't stand in the way of quick experimenting). - but both current behavior (treating it as forwarding method's args) and proposed behaviors (prohibit some of the cases for the sake of disallowing potentially confusing code) would create new types of confusion. Procs/lambdas are Ruby's main functional construct, and the main intuition is that you can do with them what you can do with methods. Breaking this intuition creates learning impediments and falls into a mental bag of "weird language quirks you just need to remember."
Are there types of code where forwarding in procs/lambdas is necessary?
I believe there are. One of the important drivers of ...
design was considering method_missing
/send
, i.e., metaprogramming.
One of the main types of metaprogramming is define_method
, which the author frequently wants to be short and expressive and allow to do everything that regular def
does.
The "start with method_missing
, and then switch to define_method
when the code matures" is one of the widespread approaches to designing various DSLs, and at that moment, the author is in a weird situation:
# code I had
def method_missing(name, **)
if ALLOWED_OPERATIONS.key?(name)
ALLOWED_OPERATIONS[name].call(**)
else
super
end
end
# code I want to replace it with:
ALLOWED_OPERATIONS.each do |name, op|
define_method(name) { |**| op.call(**) }
end
...but for some reason, I can not.
I think it feels like a bug and nothing else. And for relatively new syntax features (which meet some pushback from the community anyway), such artificial limitations might lower the adoption: "Well, I tried it once, it never worked the way I wanted it to. Don't remember the exact reason, but it was confusing, so I don't use it anymore and recommend everyone to do the same."
(Very infrequently such cases would be posted to the bug tracker/discussed in public for the core team to notice that there was such need.)
Updated by nobu (Nobuyoshi Nakada) 10 months ago
Updated by nobu (Nobuyoshi Nakada) 10 months ago
- Status changed from Assigned to Closed
Applied in changeset git|a9f096183170810ac6ce32b20d7810d11a51b5f5.
[Feature #19370] Prohibit nesting anonymous parameter forwarding