Project

General

Profile

Actions

Feature #19370

closed

Anonymous parameters for blocks?

Added by zverok (Victor Shepelev) almost 2 years ago. Updated 11 months ago.

Status:
Closed
Target version:
-
[ruby-core:111988]

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.


Related issues 1 (0 open1 closed)

Related to Ruby master - Bug #19983: Nested `*` seems incorrectClosedActions

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
Actions #6

Updated by Eregon (Benoit Daloze) about 1 year ago

  • Related to Bug #19983: Nested `*` seems incorrect added

Updated by zverok (Victor Shepelev) about 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 as def 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.)

Actions #11

Updated by nobu (Nobuyoshi Nakada) 11 months ago

  • Status changed from Assigned to Closed

Applied in changeset git|a9f096183170810ac6ce32b20d7810d11a51b5f5.


[Feature #19370] Prohibit nesting anonymous parameter forwarding

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0