Bug #20955
openSubtle differences with Proc#parameters for anonymous parameters
Description
p proc { |x| }.parameters #=> [[:opt, :x]]
p lambda { |x| }.parameters #=> [[:req, :x]]
p proc { _1 }.parameters #=> [[:opt, :_1]]
p lambda { _1 }.parameters #=> [[:req, :_1]]
p proc { it }.parameters #=> [[:opt, nil]]
p lambda { it }.parameters #=> [[:req]]
Note the last pair; here are two small confusing problems:
- For proc, unlike numbered parameters, the parameter name is
nil
(notit
). This, though, can be justified to distinguish fromproc { |it| }
case - But also,
proc
has thisnil
for a parameter name, whilelambda
has not.
I am not sure what the “most logical” thing to do here, but I believe that at least proc { it }
and lambda { it }
should be made consistent with each other.
Updated by bkuhlmann (Brooke Kuhlmann) 2 days ago
Good catch. I would expect the following behavor:
proc { it }.parameters #=> [[:opt, :it]]
lambda { it }.parameters #=> [[:req, :it]]
This would be consistent with existing behavior as shown with the x
and _1
variables. Is there a reason why it
would need to behave differently?
Updated by zverok (Victor Shepelev) 2 days ago
@bkuhlmann (Brooke Kuhlmann) The only possible reason is not to confuse with this:
proc { |it| }.parameters # the parameter is literally named "it"
I am not sure it matters much, but maybe in somebody’s metaprogramming... I don’t remember seeing libraries in Ruby that changed behavior depending on parameter names (and changing it on parameter name it
seems even less plausible), but theoretically, it could happen.
(The situation is unlike _1
, which is NOT allowed to be an explicit parameter name.)
Updated by bkuhlmann (Brooke Kuhlmann) 2 days ago
@zverok (Victor Shepelev) Makes sense...but this behavior would be nice (again, thinking in terms of consistency):
proc { |_1| }.parameters # _1 is reserved for numbered parameter (SyntaxError)
proc { |it| }.parameters # it is a reserved block parameter (SyntaxError)
I realize this would break backwards compatibility in terms of semantic versioning, though.
That said, I can provide an example of it
-- as currently implemented in Ruby 3.4.0-rc1 -- that would cause issues with my Marameters gem. ⚠️ Please note that the syntax I'm showing you is for the next major release of the gem once Ruby 3.4.0 is released:
parameters = proc { it }.parameters
signature = Marameters.signature(parameters).to_s # " = nil"
Demo = Module.new do
module_eval <<~METHOD, __FILE__, __LINE__ + 1
def self.test(#{signature}) = "This is a demo."
METHOD
end
Demo.test # syntax errors found (SyntaxError)
# ↑ This is because a method signature of `test( = nil)` is definitely not syntactically valid.
I can definitely fix my Marameters gem to account for this use case but would be nice to ensure Method#parameters
always answers an array than can immediately be used to dynamically build a new method signature that is syntactically correct.
Updated by zverok (Victor Shepelev) 2 days ago
but this behavior would be nice (again, thinking in terms of consistency):
proc { |_1| }.parameters # _1 is reserved for numbered parameter (SyntaxError)
proc { |it| }.parameters # it is a reserved block parameter (SyntaxError)
I believe that when it
was introduced, it was common understanding that it should be as non-invasive as humanly possible, so it should never be deprecated as a local variable name or explicit parameter name. It is a common short word, so amount of codebases that use it, including as an explicit name, is probably significant (as a shortcut for it
em, or "i
ndex of t
ime point" or somesuch).
it
and _1
are explicitly not, and would not be, consistent by this account and by several others (say, allowing usage in the nested blocks — though the latter seems less justified to me).
Updated by bkuhlmann (Brooke Kuhlmann) 1 day ago
Thanks. When I mentioned "consistency" I was mostly concerned about getting a parameters array from Proc#parameters
that I could use to construct an equivalent method signature but this won't be a problem with normal method signatures (well, sort of), only procs/lambdas will exhibit this behavior. I guess this is OK but does feel weird especially since you can end up with a required/optional parameter with no name. The only non-proc situation I can think of is when using a required parameter with array destructuring. Example:
def demo((one, two)) = puts "One: #{one}, Two: #{two}"
method(:demo).parameters # [[:req]]