One of the philosophical views behind Ruby is the Principle of flexibility - there are many ways to achieve the same thing. This is especially true when it comes to passing arguments to a method.

In this post, we will take a look on argument passing in Ruby and discuss pros and cons for each of the approaches.

Positional arguments

    def my_method(name, age)
      # method implementation
    end

The most basic way to pass arguments. Positional arguments have some disadvantages such as Connascence of position. Each time we change the arguments list, we have to also change all of the method invocations throughout the code due to the imposed order of arguments.

We will discuss an alternative later.

Default arguments

Methods can have default values when using positional arguments. Arguments with default values should be always on the right when mixed with arguments that have no default values.

    def my_method(name = 'John Doe', age = 39)
      # Method implementation
    end

    def my_method(name, age = 20)
      # Method implementation
    end

Unused method arguments

One naming convention that is worth mentioning. Let's look at the following example:

    def my_method(my_number, _number_two)
      2 * my_number
    end

Wonder why _number_two is prefixed with an underscore?

This is a common convention for positional arguments - we prefix with underscore method arguments that are not used in the method definition but are only present to keep the method arity consistent (the number of accepted arguments by a method).

A good practice is to keep the name of the variable and not just name it _ as this would impact the readability, especially when working on a project with a larger codebase.

Non-fixed number of arguments

We saw how to pass a fixed number of arguments. But what about passing a non-fixed number?

Consider this piece of code:

    def my_method(name, *arguments)
      # arguments.class # => Array
      # method implementation
    end

In situations like this one, my_method can accept one or more arguments.

All, except the first one, are handled by *arguments. They are just being stored into the array, referenced into the local variable arguments.

This can be handy in some particular situations. On the other hand, it may be a symptom of a code smell.

kwargs

Keyword arguments are an alternative to positional arguments. KWargs are very similar to passing a hash as an argument. Ruby has a first-class support for KWargs.

Some historical facts:

  • Required keyword arguments were added in Ruby 2.1
  • Keyword arguments must have default values in Ruby 2.0

As mentioned, methods can accept hashes as an input.

    def my_method(options = {})
      # method implementation
    end

This is the old way of managing keyword arguments but the drawback is that it requires logic for argument extraction from the given input. Luckily, kwargs solve such problems.

Default arguments

Just use the hash-like syntax in the argument list and decide whether you want default values or not.

    def my_method(my_key:, another_key: 'hakuna matata')
    end

The position of the arguments does not matter when using kwargs. Therefore, we do not need to worry about changing all of the method invocations when adding a new argument to our method.

Non-fixed number of arguments

KWargs are more strict than passing a plain hash and unknown attributes will raise an exception. This fosters traceability. The double splat operator (**) could be used on the last argument to avoid this behaviour and use the additional arguments in some way:

    def my_method(my_key: 'world', **options)
      # method implementation
    end

options references a hash that you can manage in a simmilar fashion as using the splat with positional arguments - it holds the rest of the keys that are not listed as kwargs.

Conclusion

  • Passing many arguments to a method may be a symptom of a code smell.
  • Prefer the usage of kwargs over positional arguments.
  • Prefer kwargs over passing a hash as an argument.

References