We had mentioned in one of our previous blog that the method definition in Ruby is extremely flexible. Let’s delve a little deeper into that statement and talk about the different type of arguments a method can take and how it affects its arity.
Let’s once again use the example from Marc-André Lafortune blog.
> def hi(needed, needed2,
maybe1 = "42", maybe2 = maybe1.upcase,
*args,
named1: 'hello', named2: a_method(named1, needed2),
**options,
&block)
end
> method(:hi).parameters
=> [ [:req, :needed], [:req, :needed2],
[:opt, :maybe1], [:opt, :maybe2],
[:rest, :args],
[:key, :named1], [:key, :named2],
[:keyrest, :options],
[:block, :block] ]
The **nil
argument was added in Ruby 2.7 to explicitly mark that
the method accepts no keyword arguments.
> def no_key_args(needed, *, **nil); end
> method(:no_key_args).parameters
=> [[:req, :needed], [:rest], [:nokey]]
Method arguments
Let’s use the find_by_sql
method as an example
from the ActiveRecord::Querying
module
def find_by_sql(sql, binds = [], preparable: nil, &block)
...
end
Required arguments
These can be called as the default argument type.
When you don’t know which type of argument to use, use the required arguments.
They are order dependent and as the name suggests, required.
If you don’t pass them during method invocation Ruby will throw an
ArgumentError
.
> Post.find_by_sql
=> ArgumentError: wrong number of arguments (given 0, expected 1..2)
Optional arguments
They are similar to the required arguments. The only difference is that you can set a default value in the method definition which the user can override if required.
In the find_by_sql
example, the second argument binds
is an optional argument.
> Post.find_by_sql(
'SELECT posts.* from posts where id = $1',
[[nil, 1]],
)
=> Post Load (0.6ms) SELECT posts.* from posts where id = $1 [[nil, 1]]
Variable arguments
This allows passing zero or a number of arguments. Ruby will not throw the ArgumentError if the user forgets to pass an argument. The arguments passed to the method will be placed as an array.
> def sum(*args)
args.sum
end
> sum
=> 0
> sum(1, 2, 3, 4)
=> 10
This approach can also be used to indicate that the method will accept any number of arguments but won’t do anything with them.
> def no_args_please(*)
end
> no_args_please(1, 2, 5)
=> nil
The syntax is a little different for variable keyword arguments. The arguments passed to the method will be placed as an hash.
> def options(**opts)
puts opts.inspect
end
> options(validate: false, upcase: true)
=> {:validate=>false, :upcase=>true}
Keyword arguments
Added in Ruby 2.0, they greatly improve the readability of the method definition and the calling code at the expense of more verbosity. They are not order dependent and provide very readable error messages.
> Post.find_by_sql(
'SELECT posts.* from posts where id = $1',
[[nil, 1]],
preparable: true
)
=> Post Load (0.7ms) SELECT posts.* from posts where id = $1 [[nil, 1]]
# If you mistype, Ruby will throw an ArgumentError: unknown keyword: prepare
> Post.find_by_sql(
'SELECT posts.* from posts where id = $1',
[[nil, "075b7fd3-6d48-4b30-91d4-ac174bd69d43"]],
prepare: true
)
=> ArgumentError: unknown keyword: prepare
If you don’t provide the keyword argument when it was required, Ruby will
throw an ArgumentError: missing keyword: x
> def add(x:, y: 1)
x + y
end
> add(x: 1)
=> 2
> add(x: 1, y: 2)
=> 3
> add(xx: 1)
=> ArgumentError: missing keyword: x
> add(x: 1, yy: 1)
=> ArgumentError: unknown keyword: yy
They are also easier to enhance or refactor by adding or removing arguments to methods.
Block argument
Blocks are a topic in itself, so I will just wrap it up with an example.
(1..4).each do |n|
puts "Iteration #{n}"
end
Method arity
The rules for finding the arity of the methods are as follows:
- If the method takes a fixed number of arguments, the arity will be a positive integer.
- If the method takes a variable number of arguments, the arity will be (-n-1) where n is the number of required arguments.
- Keyword arguments are counted as a single argument.
- Block is not considered as an argument.
Let’s look at some examples
# required argument
> def a_method(required_1, required_2); end
> method(:a_method).arity
=> 2
# optional argument
> def a_method(optional_1 = 1); end
> method(:a_method).arity
=> -1
> def a_method(optional_1 = 1, optional_2 = 2); end
> method(:a_method).arity
=> -1
# variable argument
> def a_method(*variable_1); end
> method(:a_method).arity
=> -1
# variable keyword argument
> def a_method(**keyrest_1); end
> method(:a_method).arity
=> -1
# keyword argument
> def a_method(keyword_1:); end
> method(:a_method).arity
=> 1
# keyword arguments are counted as 1
> def a_method(keyword_1:, keyword_2:); end
> method(:a_method).arity
=> 1
# required argument + variable argument
> def a_method(required_1, *optional_1); end
> method(:a_method).arity
=> -2
# block is not considered as an argument
> def a_method(&block); end
> method(:a_method).arity
=> 0
> def a_method(required_1, *optional_1, &block); end
> method(:a_method).arity
=> -2
# nokey argument is not considered as an argument
> def no_key_args(**nil); end
> method(:no_key_args).arity
=> 0
And finally
> def hi(needed, needed2,
maybe1 = "42", maybe2 = maybe1.upcase,
*args,
named1: 'hello', named2: a_method(named1, needed2),
**options,
&block)
end
> method(:hi).arity
=> -3