Ruby methods can define their parameters in a few different ways. Conveniently, due to some of Ruby’s metaprogramming features, we can actually look at the parameters of any method!

However, the documentation on Method#parameters is missing a few cases. In this post we will look at all types of parameters, and what Method#parameters returns for them.

Ruby’s Method Method

To start, we need to look at the Object#method method defined in Ruby’s Object class. It returns a Method object.

irb > method(:puts).class
=> Method

Method objects are super neat, and I’ll most likely write a few future posts about them. Just for a brief teaser, here are all the public methods specific to a method object:

irb > pp method(:puts).public_methods - Object.public_methods
[:unbind,
 :owner,
 :super_method,
 :<<,
 :>>,
 :to_proc,
 :[],
 :arity,
 :call,
 :curry,
 :receiver,
 :source_location,
 :parameters,
 :original_name]

Method#parameters

The method we need to focus on for now is the Method#parameters method. This is, after all, the output we’re looking to understand more deeply. It’ll tell us which parameters a method takes, and the parameter names. We can first look at a method which takes no args:

irb > method(:nil?).parameters
=> []

Straightforward enough. nil? has no parameters. We can confirm this:

irb > nil?("some argument")
....
ArgumentError (wrong number of arguments (given 1, expected 0))

What about a method which does have parameters?

irb > method(:puts).parameters
=> [[:rest]]

Hmm. We know we can supply any number of arguments to puts, so why only one parameter? And why is it in a nested array?

irb > puts "any", :number, "of", :args
any
number
of
args
=> nil

It’s because puts takes splat args. We’ll go over splat args in more depth further in this post. But, for now, we can create our own example method to confirm:

def splat_args(*splat)
  splat
end
=> :splat_args
irb > splat_args("any", :number, "of", :args)
=> ["any", :number, "of", :args]
irb > method(:splat_args).parameters
=> [[:rest, :splat]]

Hmmmm. This also has :rest in the result, but it’s not exactly the same as puts. We’re getting one parameter, but it has the name :splat. What if we left the parameter unnamed?

irb > def unnamed_splat_args(*); end
=> :unnamed_splat_args
irb > method(:unnamed_splat_args).parameters
=> [[:rest]]
irb > method(:unnamed_splat_args).parameters == method(:puts).parameters
=> true

Aha. Exactly the same. So puts has one, unnamed splat arg parameter, denoted in the returned array as :rest.

All parameters

But, there are more parameters than just splat args. Let’s make a method which has every different type of parameter, and we can see what happens.

Before we do, it’s important to cover all of the parameters that Ruby methods can take. Parameters can either:

  • be keyworded (keyworded:)or positional (positional)

    : after the parameter name will determine if it is keyworded. If so, when calling the method, we must name the argument:

      def keyworded(arg:); arg; end
      irb > keyworded(arg: "argument")
      => "argument"
    
      def positional(arg); arg; end
      irb > positional("argument")
      => "argument"
    

    When calling methods with positional arguments, the ordering of the arguments matters. We must supply the arguments in the order they are named. When calling methods with keyword arguments, the order of calling does not matter.

  • have default values or no default values:

    If an argument does not have a default value, it must be passed. If it does have a default value, it is optional If the argument is keyworded, the default value simply follows the colon:(keyworded: "default") If it’s not keyworded, the syntax is (not_keyworded = "default")

     def default(arg = "default"); arg; end
     irb > default
     => "default"
     irb > default("some other value")
     => "some other value"
    
     def no_default(arg); arg; end
     irb > no_default
     ArgumentError (wrong number of arguments (given 0, expected 1))
     irb > no_default("some value")
     => "some value"
    
  • be splat args (unlimited number of arguments)

    Ruby also allows for methods which can take a variable number of args, using the * operator. If these arguments are not keyworded, they will evaluate to an array:

     def splat_args(*splat); splat; end
     irb > splat_args("any", :number, "of", :args)
     => ["any", :number, "of", :args]
    

    If they are keyworded, we use the double splat ** operator, and they will evaluate to a hash:

     def double_splat_args(**double_splat); double_splat; end
     irb > double_splat_args(some: "keyword", args: nil)
     => {:some=>"keyword", :args=>nil}
    

    Note that we cannot pass keyworded args to a method expecting a splat:

     irb > double_splat_args("not keyworded")
     ArgumentError (wrong number of arguments (given 1, expected 0))
    

    And passing keyworded args to a method with a splat parameter will result in a hash for that argument in the array of args:

     irb > splat_args(keyworded: "args")
     => [{:keyworded=>"args"}]
    
  • be block args

    The last type of argument we can pass is a block, denoted with an &:

      def block_args(&block); yield; end
      irb > block_args { "calling the block" }
      => "calling the block"
    

Back to Method#parameters

This has all really been buildup for Method#parameters. How do all of these combinations of keyword / positional / optional / required / splat / double splat / block args actually look when we call Method#parameters? To find out, let’s write a jumbo method, passing all types of arguments:

def jumbo_method(
    required_positional,
    optional_positional = nil,
    *splat_args,
    required_keyword:,
    optional_keyword: nil,
    **double_splat_args,
    &block
)

<<-ARGS
    required_positional: #{required_positional}
    optional_positional: #{optional_positional}
    splat_args: #{ssplat_args}
    required_keyword: #{required_keyword}
    optional_keyword: #{optional_keyword}
    double_splat_args: #{double_splat_args}
    block: #{yield}
ARGS
end

puts jumbo_method(
    "required_positional",
    "optional_positional",
    "any", "number", "of", "positional", "args",
    required_keyword: "required_keyword",
    optional_keyword: "optional_keyword",
    any: "number", of: "keyword", args: "!",
) { "block" }

=> required_positional: required_positional
optional_positional: optional_positional
splat_args: ["any", "number", "of", "positional", "args"]
required_keyword: required_keyword
optional_keyword: optional_keyword
double_splat_args: {:any=>"number", :of=>"keyword", :args=>"!"}
block: block

and calling .parameters on this method we’ll get:

irb > method(:jumbo_method).parameters
[[:req, :required_positional],
[:opt, :optional_positional],
[:rest, :splat_args],
[:keyreq, :required_keyword],
[:key, :optional_keyword],
[:keyrest, :double_splat_args],
[:block, :block]]

This is the output we were looking for! The array of paramaters contains arrays of size two where the first element is the type of parameter, and the second is the name of the parameter. Since we named all of our parameters descriptively, we can use it to see exactly how Method#parameters refers to each type. For example, optional positional parameters are :opt while optional keyword parameters are :key.