How to pass arguments to methods in ruby and how it affects their arity


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 explicity 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