Anton K Anton K - 2 months ago 5
Ruby Question

Ruby required argument assignment logic: how does the ordering works?

in Ruby, required arguments have the highest priority when they are being assigned in method. Example:

arg_demo(a, b, c=1, *d, e, f)


here, a,b,c,e,f have higher priority than array of arguments *d.

when we call this:

arg_demo(1, 2, 3, 4, 5, 6, 7, 8)


the corresponding parameters are:
a=1, b=2,c=3, *d=[4,5,6], e=7,f=8
.
In this case, the *d gets its parameters assigned as the last one.

If that is the case, how does Ruby know to assign e to 7, and f to 8 (as if "reserving" the 4,5,6 to the lower priority *d)?

Answer

Ruby provides a great deal of flexibility in how a method's arguments are defined. She cannot read your mind, however, so she'll raise an exception if the assignment of arguments to variables is incorrect or ambiguous. The basic rule is that Ruby can work out how the values passed to a method should be assigned to arguments if and only if you can work that out yourself, and there is only one way it could be done (subject to Rule 4 below). We can formalize this with the first three rules below.

If these rules are satisfied, it should be obvious from the discussion how the values passed to the method are assigned to arguments, though Rule 4, which is not obvious, is also needed.

  • Rule 1: required arguments must be preceded or followed by zero or more required variables, and nothing else (i.e., they must be stacked at the beginning and/or end of the list of values passed to the method).

  • Rule 2: If all of a method's arguments are required or have default values, the number of values passed to the method must be between n and n+m, where n is the number of required arguments and m is the number of arguments with defaults.

  • Rule 3: A splatted argument cannot precede a splatted argument or an argument with a default value.

  • Rule 4: If an argument a with a default value is followed by a splatted argument or another argument with a default value, and the above rules are satisfied, the first value passed to the method that has not already been assigned to a preceding argument is assigned to a.

Assignment of arguments to required variables

Ruby first checks that there is exactly one way that required arguments can be assigned to their associated variables.

Here are some examples where Ruby gives a thumbs-up.

def meth(w,x)
  [w,x]
end
meth 1,2
  #=> [1,2]

I gave the above for completeness.

def meth(w,x,*y,z)
  [w,x,y,z]
end
meth 1,2,3,4,5,6
  #=> [1, 2, [3, 4, 5], 6]

w and z are the first and last arguments, and x is preceded by one or more required arguments and nothing else.

def meth(w,x=1,y,z)
   [w,x,y,z]
end
meth 1,2,3,4
  #=> [1, 2, 3, 4] 
meth 1,2,3
  #=> [1, 1, 2, 3] 

w and z are the first and last arguments, and y is followed by one or more required arguments and nothing else.

Now some examples where Ruby finds the assignment of variables to arguments is ambiguous, and therefore raises an exception (at compile-time).

def meth(*x,y,*z)
  [x,y,z]
end
  #=> syntax error, unexpected keyword_end, expecting end-of-input

The reason is obvious.

def meth(w,x=1,y,*z)
  [w,x,y,z]
end
  #=> syntax error, unexpected keyword_end, expecting end-of-input

Ruby doesn't know if x should equal its default or the second value passed to the method, which affects both y and *z.

Note that if the first (last) argument is required, the first (last) value passed to the method is assigned to that argument.

This establishes Rule #1.

When all arguments are required or have default values

def meth(w,x,y=3,z)
  [w,x,y,z]       
end
meth 1,2,3,4
  #=> [1, 2, 3, 4] 
meth 1,2,3
  #=> [1, 2, 3, 3] 
meth 1,2
  #=> ArgumentError: wrong number of arguments (given 2, expected 3..4)
meth 1,2,3,4,5
  #=> ArgumentError: wrong number of arguments (given 5, expected 3..4)

This establishes Rule #2.

Non-required arguments

If the method passes the test for required arguments, we can remove the associated variables from the argument list and then determine if Ruby would be happy with what's left (since the required arguments are stacked at the beginning and/or end of the argument list).

Two examples:

def meth(*w,*x)
end
  #=> syntax error, unexpected *

def meth(*w,x=1)
end
  #=> syntax error, unexpected '=', expecting ')'

This establishes Rule #3.

Remaining possibilities

Consider an argument with a default value that precedes either a splatted argument or another argument with a default value.

def meth(w=1,x=2,*y)
  [w,x,y]
end
meth
  #=> [1, 2, []] 
meth 3
  #=> [3, 2, []] 
meth 3,4
  #=> [3, 4, []] 
meth 3,4,5
  #=> [3, 4, [5]] 
meth 3,4,5,6
  #=> [3, 4, [5, 6]] 

def meth(w=1,x=2)
  [w,x]
end
meth
  #=> [1, 2] 
meth 3
  #=> [3, 2] 
meth 3,4
  #=> [3, 4] 

We see that the first values passed to the method will be assigned to the arguments with the default values. If no values are passed to the method both variables will be assigned to their default values.

This gives us Rule 4.

Design decision

Note that the Ruby monks could have decided to raise a compile-time exception for def meth(w=1,*x) and/or a run-time exception for def meth(w=1,x=2) when only one value is passed, but chose not to do so.

Common practice

As a matter of common practice, one always see the required arguments (if any) listed first, followed by zero or more arguments with default values, followed by zero or one splatted arguments. There is no loss of functionality by ordering the variables in that way.