def demo(*, **, &) = super
ap method(:demo).parameters
# [
# :rest,
# :*
# ],
# [
# :keyrest,
# :**
# ],
# [
# :block,
# :&
# ]
Prior to the release of Ruby 3.2.0, bare splats never had a name so you’d only get information about the kind of splat which meant you couldn’t do much else with them. With Ruby 3.2.0 and higher, bare splats can be forwarded in addition to other benefits:
You can use super
— as shown in the example above — to pass the splatted arguments upwards for further processing.
You can forward the single, double, and/or block splat to another method. Example: def demo(*, **, &) = my_method(*, **, &)
.
You can gobble positional and/or keyword arguments into a black hole so-to-speak so they are never used. This can be handy when coupled with the Null Object Pattern where you ignore the incoming splatted arguments entirely.
def demo(*, **, &) = puts "Positionals: #{[*]}, Keywords: #{{**}}, Block: #{proc(&)}."
demo(1, 2, c: 3, d: 4) { "example" }
# Positionals: [1, 2], Keywords: {:c=>3, :d=>4}, Block: #<Proc:0x000000012c293128 (irb):5>.
With the positional and keyword arguments, you get immediate inspection. With the block, you get partial inspection because you can see if you are dealing with a proc or a lambda. Alternatively, you could call the block for deeper inspection:
def demo(*, **, &) = puts "Positionals: #{[*]}, Keywords: #{{**}}, Block: #{proc(&).call}."
demo(1, 2, c: 3, d: 4) { "example" }
# Positionals: [1, 2], Keywords: {:c=>3, :d=>4}, Block: example.
As you can see, we get kind and name information which means we can make use of them within the method like any other argument.
The difference between forwards, anonymous and named splats is important to know when inspecting a method’s behavior or dynamically building a list of arguments for messaging.
Empty
Single and double splats can be implicitly coerced into an Array
and Hash
, respectively, when given no arguments. Example:
def demo(*positionals, **keywords) = puts "Positionals: #{positionals}, Keywords: #{keywords}"
demo # Positionals: [], Keywords: {}
Behavior is different when passing nil
, though. Example:
demo *nil # Positionals: [], Keywords: {}
demo **nil # no implicit conversion of nil into Hash (TypeError)
At the moment — and unlike an Array
— **nil
can’t be implicitly cast into a Hash
and will raise a TypeError
. Starting in Ruby 3.4.0, this is fixed so you can pass *nil
and **nil
as arguments and, implicitly, get an empty array and hash.
You will get a RuboCop Style/OptionalArguments
error if attempting to use this parameter.
You can’t use a single splat positional argument after a post parameter is defined. Although, you can use required, optional, and double splat keyword parameters as well as a block.
Performance-wise, Ruby can’t optimize post parameters like it can for required and optional parameters.
def demo(positional_name = nil, name: positional_name) = puts "Name: #{name}."
demo "Test" # "Name: Test"
demo name: "Test" # "Name: Test"
Notice you can pass in the "Test"
value either as a positional argument or keyword argument and get the same result. In the case of using a positional argument, the value will become the default value for the keyword parameter. This can be handy when wanting to use a positional parameter as a fallback for your keyword parameter or wanting to refactor using keyword parameters instead of positional parameters. While this is worth knowing, there are several caveats to watch out for since this can lead to problematic code:
When passing in both a positional and keyword argument, the keyword argument will take precedence due to being evaluated last. For example, using demo "One", name: "Two"
will print the value of "Two"
instead of "One"
.
When wanting to convert multiple positional parameters into keyword parameters, you can end up with a very long method signature that will quickly violate the rule of three for maximum number of parameters used.
You should add a deprecation warning when the positional parameter is set to encourage people to update their code to using the keyword arguments instead. This will allow people to upgrade while also setting a date in which you delete positional parameter support to keep code complexity at a minimum.
def demo begin: 1, end: 10
puts "Begin: #{binding.local_variable_get(:begin)}, End: #{binding.local_variable_get(:end)}."
demo # "Begin: 1, End: 10."
You can clean this up further by using the Refinements gem:
require "refinements/binding"
using Refinements::Binding
def demo(begin: 1, end: 10) = puts "Begin: #{binding[:begin]}, End: #{binding[:end]}."
# "Begin: 1, End: 10."
If refining isn’t your cup of team, you could use hash pinning:
def demo begin: 1, end: 10
attributes = {begin:, end:}
puts "Begin: #{attributes[:begin]}, End: #{attributes[:end]}."
demo # "Begin: 1, End: 10."
While hash pinning might be slightly awkward, there can be a performance boost as shown in these benchmarks.
While the above provides a working solution, there are important caveats to be aware of:
This solution is only possible when using keyword parameters and does not work for positional parameters.
There is precedence for this use case but even the core language is not the best teacher so tread with caution.
Applying this solution to your own code should be done with caution because heavy use of binding
can get unwieldy and cumbersome quickly. Granted, this can be useful in Domain Specific Languages (DSLs) or adding a touch of syntactic sugar to your method signatures in rare situations but you’d only want to reach for this when all other options have been exhausted.
This usage is more an antipattern because the required positional parameter (array) is subtle and will cause people to have a hard time reading and understanding the code. At the same time, this does give you a way to define the shape of the required positional parameter. Other complications, such as shared assignment, is another concern. Take the following:
def demo_a(first, second = first) = [first, second]
def demo_b((first, second), third = first) = [first, second, third]
# Default assignment works as expected.
demo_a 1 # [1, 1]
# Default assignment fails due to a bug with parameter destructuring.
demo_b [1, 2] # [1, 2, nil]
In general — and due to these complications — use of parenthesis is not a recommended practice.
Primitives
Ruby primitives — such as String
, Integer
, Struct
, and so forth — are written in C, not Ruby. Sadly, this means asking for a primitive’s parameters will give you false information. A good example is using an inline Struct
:
ap Struct.new(:one, :two).method(:new).parameters
# [
# :rest
# ]
If you use Structs a lot — or any primitive written in C — you know this information is incorrect because a struct can accept both positional and keyword arguments. The problem is the argument conversion is done in C, not Ruby. This is another aspect of Ruby where asking for a method’s parameters won’t help you but is important to be aware of. There is a long standing Ruby Issue, if fixed, would solve this problem and lead to more informative parameter information.
One trick to solving this situation is via a .for
method (or whatever method you deem is an appropriate shim). Example:
Demo = Struct.new :one, :two do
def self.for(...) = new(...)
ap Demo.method(:for).parameters
# [
# :rest,
# :*
# ],
# [
# :keyrest,
# :**
# ],
# [
# :block,
# :&
# ]
Notice how we get better parameter information — well, minus the block parameter — which tells the truth in terms of being able to use positional or keyword parameters. To take this a step further by being specific, use splats instead. Example:
# Positional Only. Parameters: `[[:rest, :*]]`.
Demo = Struct.new :one, :two do
def self.for(*) = new(*)
# Keyword Only. Parameters: `[[:keyrest, :**]]`.
Demo = Struct.new :one, :two do
def self.for(**) = new(**)
In all of these examples, you now have more accurate parameter information via .for
instead of .new
which comes in handy when dynamically messaging these objects and using a gem like Marameters, for example.
Procs and Lambdas
Starting with Ruby 3.2.0, support was added for proc parameters to be converted into lambda parameters for quick proc to lambda conversion. Example:
demo = proc { |one, two = 2| [one, two] }
demo.parameters # [[:opt, :one], [:opt, :two]]
demo.parameters lambda: true # [[:req, :one], [:opt, :two]]
Notice, by passing lambda: true
to the #parameters
method, the proc will answer back parameters as if the proc was a lambda since the first parameter would be required for a lambda but optional for a proc. This provides a way for you to dynamically convert a proc into a lambda.
One caveat to be aware of with procs/lambas is: argument forwarding. Currently, argument forwarding isn’t possible. Consider the following:
inspector = lambda do |*positionals, **keywords, &block|
puts "Positionals: {#positionals}, Keywords: #{keywords}, Block: #{block}"
# Syntax Error
proc { |...| inspector.call(...) }
# unexpected (..., expecting '|'
# Syntax Error
proc { |*, **, &| inspector.call(*, **, &) }
# no anonymous rest parameter
# no anonymous keyword rest parameter
# no anonymous block parameter
# Named parameters are OK to be forwarded.
demo = proc do |*positionals, **keywords, &block|
inspector.call(*positionals, **keywords, &block)
demo.call(1, a: 1) { "example" }
# Positionals: {#positionals}, Keywords: {:a=>1}, Block: #<Proc:0x00000001191108b8 /demo:105>
Anonymous argument forwarding might be supported in the future so keep an eye on future versions of Ruby to see how this evolves.
module Operation
def self.demo one, two = :b, *three, four:, five: :e, **six, &seven
puts <<~ARGUMENTS
1 (reg): #{one.inspect}
2 (opt): #{two.inspect}
3 (rest): #{three.inspect}
4 (keyreq): #{four.inspect}
5 (key): #{five.inspect}
6 (keyrest): #{six.inspect}
7 (block): #{seven.inspect}
ARGUMENTS
Keep this implementation in mind or scroll back, when needed, to reread since this implementation will be referenced repeatedly.
Basic
With the above implementation, the minimal set of arguments we could use are:
Operation.demo :a, four: :d
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): []
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {}
# 7 (block): nil
All seven arguments — a few of which are either nil
or empty since they are optional — are printed to the console. This output can be further explained as follows:
opt: b
: This positional argument was optional so the default value of :b
was answered back.
rest: []
: This positional argument was optional too but since we gave it nothing, we got an empty array due to single splats always resolving to an array.
keyreq: d
: This keyword argument was required so we had to pass in a value (i.e. :d
).
key: e
: This keyword argument was optional so the default value of :e
was answered back.
keyrest: {}
: This keyword argument was optional too but since we gave it nothing, we got an empty hash due to double splats always resolving to a hash.
block: nil
: We didn’t supply a block so we got back nil in this case since blocks are optional since they can be implicitly or explicitly used.
function = proc { "test" }
Operation.demo :a, :b, :y, :z, four: :d, y: 10, z: 20, &function
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000109679ad0 /snippet:32>
This time we have a lot more arguments passed in and printed out to the console. We can break this down further highlighting the differences:
rest: [:y, :z]
: Any number of optional positional arguments could have been supplied here but only :y
and :z
were used in this case.
keyreq: d
key: e
keyrest: {:y ⇒ 10, :z ⇒ 20}
: Any number of optional keyword arguments could have been supplied here but only y: 10
and z: 20
were used in this case.
block: #<Proc:0x000000010d1cbc78>
: Since we passed in an explicit block, you can see it’s pass through and printed out as well.
Single (array): Evaluates to *positionals.to_a
. This behavior is definitely surprising and an oddity because you’d expect the implicit #to_ary
to be called instead of the explicit #to_a
which is inconsistent with how double and block arguments work.
Double (hash): Evaluates to **keywords.to_hash
.
Block (proc): Evaluates to &block.to_proc
.
💡 For more information on proper use of implicit and explicit casting, see my Ruby Antipatterns articles for details.
You want to splat your arguments when you need to destruct your arrays, hashes, or proc into a list of arguments or can’t directly pass them to a method but need to dynamically build up the arguments instead. With the .demo
method implementation from earlier — and using our knowledge of positional, keyword, and block parameters — we can pass single and double splats along with a block to this method as follows:
Operation.demo *%i[a b y z], **{four: :d, y: 10, z: 20}, &function
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000119418d58 /snippet:32>
Notice the above’s output is identical to our earlier example (except for the new Proc
instance) where we passed in a maximum set of arguments. What’s different is we’ve categorized the positional, keyword, and block arguments. Single and double splats makes this easier. To take this a step further — and assuming the argument list was dynamically assigned to local variables — the code then becomes more descriptive:
function = proc { "test" }
positionals = %i[a b y z]
keywords = {four: :d, y: 10, z: 20}
Operation.demo *positionals, **keywords, &function
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000109c59810 /snippet:32>
Keep in mind that single and double splats must be used when destructuring arrays and hashes for messaging purposes. For example, the following doesn’t work because you have to explicitly use single or double splats with your arguments:
Operation.demo positionals, keywords, &function
# missing keyword: :four (ArgumentError)
We’ll learn more about splats in message delegation next.
Super
The super
keyword is one of several Ruby Keywords that allows you to forward arguments to the parent method when using inheritance. The super
keyword deserves special mention since behavior can differ based on the types of arguments used. Consider this basic implementation:
class Parent
def demo(*positionals, **keywords, &block)
puts "Positionals: #{positionals}, Keywords: #{keywords}, Block: #{block}"
class Child < Parent
def demo(*positionals, **keywords, &block) = super
child = Child.new
child.demo(1, b: 2) { :example }
# Positionals: [1], Keywords: {:b=>2}, Block: #<Proc:0x000000013e9d9ba0>
The above shouldn’t be surprising when forwarding arguments from the child to the parent because super
— without explicit arguments or parenthesis — implicitly forwards the same arguments. Same goes if you modify the arguments before calling super
:
class Child < Parent
def demo(*positionals, **keywords, &block)
positionals.append :a
keywords[:b] = 2
super
Child.new.demo { :example }
# Positionals: [:a], Keywords: {:b=>2}, Block: #<Proc:0x0000000133c190b0>
However, using parenthesis, will prevent super
from forwarding any arguments:
class Child < Parent
def demo(*positionals, **keywords, &block) = super()
child = Child.new
child.demo(1, b: 2) { :example }
# Positionals: [], Keywords: {}, Block: #<Proc:0x0000000148034208>
As you can see, the positional and keyword arguments were not forwarded but notice the block was! This is because super
implicitly forwards the block whether you like it or not. To prevent this behavior, use super(&nil)
:
class Child < Parent
def demo(*positionals, **keywords, &block) = super(&nil)
child = Child.new
child.demo(1, b: 2) { :example }
# Positionals: [], Keywords: {}, Block:
# frozen_string_literal: true
# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "amazing_print"
gem "debug"
gem "dry-monads"
gem "marameters"
include Dry::Monads[:result]
function = proc { "test" }
module Operation
def self.demo one, two = :b, *three, four:, five: :e, **six, &seven
puts <<~ARGUMENTS
1 (reg): #{one.inspect}
2 (opt): #{two.inspect}
3 (rest): #{three.inspect}
4 (keyreq): #{four.inspect}
5 (key): #{five.inspect}
6 (keyrest): #{six.inspect}
7 (block): #{seven.inspect}
ARGUMENTS
class Exampler
def initialize operation, method, marameters: Marameters
@operation = operation
@method = method
@marameters = marameters
def first_example(...)
operation.public_send(method, ...)
def second_example(*, **, &)
operation.public_send(method, *, **, &)
def third_example *positionals, **keywords, &block
operation.public_send method, *positionals, **keywords, &block
def fourth_example arguments
positionals, keywords, block = arguments
operation.public_send method, *positionals, **keywords, &block
def fifth_example result
result.fmap do |positionals, keywords, block|
operation.public_send method, *positionals, **keywords, &block
def sixth_example arguments
marameters.categorize(operation.method(method).parameters, arguments)
.then do |splat|
operation.public_send method, *splat.positionals, **splat.keywords, &splat.block
private
attr_reader :operation, :method, :marameters
The major changes are that:
Monads are used. Don’t worry about needing to know how to use monads. You only need to care about the destructuring of arguments in the #fifth_example
method. I’ll explain, shortly.
The Marameters gem is introduced as well which provides a convenient way to obtain method parameter information. We’ll talk about this gem in the final example.
Now we can focus on the #*_example
methods in terms of argument forwarding, splats, and destructured arguments.
Argument Forwarding
Argument forwarding (i.e. ...
) makes passing of arguments less of a hassle in some cases. To see argument forwarding in action, here’s how make use of the above code snippet:
exampler = Exampler.new Operation, :demo
exampler.first_example :a, :b, :y, :z, four: :d, y: 10, z: 20, &function
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000109675980 /snippet:32>
By now, the output should come as no surprise since all we’ve done is forward all arguments from the #first_example
method to the .demo
method by adding a leading argument to specify the :demo
method when using #public_send
. This is a elegant and the most succinct of all examples.
Splats
Should argument forwarding not be desired or needed, we can always fall back to using splats as provided by the second and third examples. We’ll start with the second example:
exampler = Exampler.new Operation, :demo
exampler.second_example :a, :b, :y, :z, four: :d, y: 10, z: 20, &function
# 1 (reg): a
# 2 (opt): b
# 3 (rest): [:y, :z]
# 4 (keyreq): d
# 5 (key): e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x000000010b7cbee0 /snippet:18>
As you can see from the output, there is no change between the first and second example. What might be surprising is you can use anonymous single splat, double splat, and block parameters. This is new in Ruby 3.2.0. If we want to be more verbose, we can name our arguments as done in the third example:
exampler = Exampler.new Operation, :demo
exampler.third_example :a, :b, :y, :z, four: :d, y: 10, z: 20, &function
Again, no change in output as we get the same desired result but this time we’ve given our single splat, double splat, and block arguments a name.
A subtle distinction is that you must use splats in your parameters and continue to use splats when delegating the arguments, otherwise you’ll end up with nested arrays and/or hashes. Here are a few examples of the errors you’ll get when the code is modified without splats:
# No parameter splats.
def third_example positionals, keywords, &block
operation.public_send method, *positionals, **keywords, &block
# Yields: wrong number of arguments (given 5, expected 2) (ArgumentError)
# No argument splats.
def third_example *positionals, **keywords, &block
operation.public_send method, positionals, keywords, &block
# Yields: `demo': missing keyword: :four (ArgumentError)
exampler = Exampler.new Operation, :demo
exampler.fourth_example [%i[a b y z], {four: :d, five: :e, y: 10, z: 20}, function]
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x00000001091916a8 /snippet:32>
In this case, the single array argument is composed of positional, keyword, and block elements which are destructured into positionals
, keywords
, block
local variables so they can be passed on using a single splat, double splat, and ampersand. There is no way to dynamically use *
, **
, or &
unless you evaluate the expression as a string. Example:
def fourth_example arguments
positionals, keywords, block = arguments
instance_eval "operation.#{method} *positionals, **keywords, &block", __FILE__, __LINE__
Instance evaluation would be an unnecessary — and less performant — so prefixing the local variables with *
, **
, and &
is better. A similar pattern is used when passing a monad as an argument to the fourth method:
exampler = Exampler.new Operation, :demo
exampler.fifth_example Success([%i[a b y z], {four: :d, five: :e, y: 10, z: 20}, function])
# 1 (reg): a
# 2 (opt): b
# 3 (rest): [:y, :z]
# 4 (keyreq): d
# 5 (key): e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000109ba35b0 /snippet:18>
The key difference is that we can use the block parameters to automatically destructure into positional, keyword, and block arguments needed for message passing. 🎉
Sadly, we can’t use argument forwarding in block parameters. It would be nice but is currently not possible.
Marameters
At the start of this article, I alluded to the Marameters gem. Specifically, the .categorize
method used in the #sixth_example
method. Using everything we’ve discussed above, Marameters will ensure the arguments align as required by the method’s parameters. Example:
exampler = Exampler.new Operation, :demo
exampler.sixth_example [:a, :b, %i[y z], {four: :d}, nil, {y: 10, z: 20}, function]
# 1 (reg): :a
# 2 (opt): :b
# 3 (rest): [:y, :z]
# 4 (keyreq): :d
# 5 (key): :e
# 6 (keyrest): {:y=>10, :z=>20}
# 7 (block): #<Proc:0x0000000109370b40 /snippet:32>
As you can see, the output is the same as all of our earlier examples. No change and no surprise. However — and this is important to be compatible with Method#parameters — we use a single array argument which means all elements of the array must be in the right position to match the equivalent positions of the method’s parameters.
The Marameters gem can be handy when you want to build an array of arguments for forwarding to a method. Definitely check out the gem’s documentation for more details.
During the course of this article we’ve learned how to parse a methods’s parameters, along with the kinds of parameters you can use in a method signature, and how the arguments passed to the method are parsed based on the defined parameters. We’ve also learned, briefly, how to leverage the Marameters gem to dynamically build a list of method arguments. Hopefully, with this knowledge, you’ll be able to leverage better use of parameters and arguments in your own implementations too. Enjoy!