Creating functions is fundamental in our developer profession. With Ruby, we are fortunate to have a language that is very flexible in composing the definition of these methods.
In this article, we will explore all the different types of arguments a function can receive.
Table of Contents
- Positional Arguments
- Keyword Arguments
- When to use Positional / Keyword?
- Named Rest Arguments
- Anonymous Rest Arguments
- Nil Splat Operator Arguments
- Block Argument
Positional Arguments
Positional Arguments are the most commonly used and are defined in the order they appear. They are directly related to the order of method parameters. Let's look at an example:
def add(a, b, c)
puts a + b + c
end
add(1, 2, 3) # => 6
You can also assign them a default value with this syntax:
def add(a, b = 2, c = 3)
puts a + b + c
end
add(1) # => 6
⚠️ Note: It is impossible to wrap a Positional Argument around two Positional Arguments with a default value.
def add(a = 1, b, c = 3) # => SyntaxError
puts a + b + c
end
Keyword Arguments
Keyword arguments allow you to explicitly specify which variable each value is intended for. This makes the method call more readable and flexible. Here's how it works:
def greet(name:, age:)
puts "Hello #{name}! You are #{age} years old."
end
greet(name: "Alice", age: 30) # => Hello Alice! You are 30 years old.
You can mix Positional and Keyword arguments:
def greet(name, age:)
puts "Hello #{name}! You are #{age} years old."
end
greet("Alice", age: 30) # => Hello Alice! You are 30 years old.
⚠️ Note: It is impossible to place a Positional argument after a Keyword Argument.
def greet(name:, age) # => SyntaxError
puts "Hello #{name}! You are #{age} years old."
end
The great advantage of using Keywords is that it does not allocate memory for the Hash sent to it. I'll explain this with an example:
def greet(options = {}) # will allocate memory if no args
puts "Hello #{options.fetch(:name)}! You are #{options.fetch(:age)} years old."
end
greet({name: "Alicia", age: 33}) # you just created a hash!
def greet(name:, age:) # won't allocate memory if no args
puts "Hello #{name}! You are #{age} years old."
end
greet(name: "Alicia", age: 33) # no memory allocation
Positional and Keywords are the two most commonly used types of arguments. So far, we haven't seen the true power of Ruby. Before we go further, let's take a moment to recall some rules regarding the use of Positional and Keywords arguments.
When to use Positional / Keyword?
For this, I have a simple rule:
- If my method is public → Keyword Argument
Examples of methods called outside the context of my class, the
initialize
method, thecall
method for Service Objects. - If my method is private → Positional Argument Methods internal to a class are there to unload actions performed in public methods. Since we have total control over callers and callees, we can afford to use only Positional Arguments.
Named Rest Arguments
The Splat Operator (*
) and the Double Splat Operator (**
) help us manage a variable number of arguments to receive in our function.
Let's look at an example:
def display_info(*args, **kwargs)
puts "Positional Arguments: #{args}"
puts "Keyword Arguments: #{kwargs}"
end
display_info(1, 2, {fake: :love}, name: "Bob", age: 25)
# => Positional Arguments: [1, 2, {:fake=>:love}]
# => Keyword Arguments: {:name=>"Bob", :age=>25}
-
*args
means: Take all Positional Arguments and put them into theargs
variable as a list. -
**kwargs
means: Take all Keyword Arguments and put them into thekwargs
variable as a Hash.
args
and kwargs
are named as such by convention; you are free to rename them as you see fit.
Anonymous Rest Arguments
In the previous case, we saw what happens when we name our use of Splat Operators in our method definition.
In reality, there is a second use for splat operators when we don't assign them to anything:
def process_data(*, processing_method: "default")
puts "Processing with the method: #{processing_method}"
end
process_data("Hello", "World", processing_method: "custom")
# => Processing with the method: custom
In the example above, you saw that nothing was done with the sent Positional Arguments.
The behavior is the same with the Double Splat Operator:
def process_data(card, method, **)
puts "#{card} was debited with #{method} method"
end
process_data("Hello", "World", processing_method: "custom")
# => Hello was debited with World method
But then you must be wondering, what's the use?
- Delegate to underlying functions by ignoring the content of the arguments.
- Ignore "extra" arguments and avoid raising an exception.
Let's look at an example that explains these two use cases:
def greet(**)
greet_kindly(**)
greet_wickedly(**)
end
def greet_kindly(name:, **)
puts "Hi #{name}, nice to see you!"
end
def greet_wickedly(name:, surname:, **)
puts "Hi #{name}, what a stupid surname #{surname}..."
end
greet(name: "Hugo", surname: "The V")
# => Hi Hugo, nice to see you!
# => Hi Hugo, what a stupid surname The V...
The greet_kindly
method received name
and surname
but extracted name
and sent surname
into the void thanks to the **
notation. Without **
, an ArgumentError
would be raised.
There is another method for delegating arguments; it's called Argument Forwarding, with the ...
notation:
def greet(...)
greet_kindly(...)
greet_wickedly(..)
end
[...]
The only difference is that *
and **
only look at one type of argument (Positional / Keyword), while ...
considers all types of arguments.
Nil Splat Operator Arguments
If we combine the Double Splat Operator and nil
, what do you think happens?
def greet(name, **nil)
puts "Hello #{name}!"
end
greet("Charlie", other: "argument?") # => ???
I'll give you the answer: an exception no keywords accepted (ArgumentError)
is raised.
How is this useful?
Well, you will be surprised to learn that Keyword Arguments have a very interesting property. If the method expects a Positional Argument, and a Keyword Argument is sent, the Keyword Argument is cast into a Hash and promoted to the rank of Position Argument.
def greet(name)
puts "Hello #{name}!"
end
greet(name: "Hugo") # => Hello {:name=>"Hugo"}
If you want to prevent this behavior in your methods, you need to add **nil
at the end of your parameter declaration!
Block Argument
This section could be an entire article. Managing Blocks is what makes Ruby so appreciated.
The promise of the Block Argument is to be able to delegate a part of the internal body of your method externally. This offers incredible flexibility. The best part? You can directly send information to the Block:
def do_action
yield(1, 2)
end
do_action do |a, b|
pp "What does #{a} + #{b} ? #{a+b}"
end
In this example, I used my Block anonymously, but you can also name it as follows:
def do_action(&block)
yield block
end
do_action do
"Hello World!"
end
# => Hello World!
These are very simple examples, but keep in mind that Block Arguments open up a huge range of possibilities in your applications.
TL;DR
Here's a concise summary of each type of argument covered today!
-
Positional Arguments
(a, b, c)
-
Keyword Arguments
(name:, age:)
- Named Rest Arguments
(*args, **kwargs)
-
Anonymous Rest Arguments
(*, **)
-
Argument Forwarding
(...)
-
nil kwargs
(**nil)
-
Block Argument
(&block)
That's it for today!
Feel free to experiment with these different types of arguments in your own Ruby projects and share your discoveries. By mastering these subtleties, you will be better equipped to design functions that are more robust and flexible, truly tailored to the needs of your applications.
Top comments (0)