The letter A styled as Alchemists logo. lchemists
Published September 1, 2022 Updated January 2, 2024
Cover
Ruby Method Parameters And Arguments

The distinction between method parameters and arguments — as shown in the screenshot above — is probably familiar to most Ruby engineers. If not, here’s a quick explanation on the distinction between the two as pulled from the Marameters gem:

  • Parameters: Represents the expected values to be passed to a method when messaged as defined when the method is implemented. Example: def demo one, two: nil.

  • Arguments: Represents the actual values passed to the method when messaged. Example: demo 1, two: 2.

With the above in mind, this article will dig deeper into how parameters and arguments work in Ruby since there are subtleties that might not be obvious unless you’re doing more complex logic and/or metaprogramming.

Parameters

There is important terminology to be aware of when defining method parameters and later passing arguments to the same method. I’ve split the terminology into several sections starting at a high level and working further down into specifics.

Basic

Basic parameters are those which you are most likely to be aware of and use on a daily basis. Consider the following code where all possible parameters are used:

def demo(one, two = :b, *three, four:, five: :e, **six, &seven) = super

The above leverages the basic parameters you can define for a method in order of precedence. If we inspect the method’s parameters and then use Amazing Print to render the response, things become more clear since we get an array of tuples (i.e. kind and name):

ap method(:demo).parameters

# [
#   [
#     :req,
#     :one
#   ],
#   [
#     :opt,
#     :two
#   ],
#   [
#     :rest,
#     :three
#   ],
#   [
#     :keyreq,
#     :four
#   ],
#   [
#     :key,
#     :five
#   ],
#   [
#     :keyrest,
#     :six
#   ],
#   [
#     :block,
#     :seven
#   ]
# ]

The kinds of parameters allowed can be split into three categories:

  • Positionals: Positional parameters are more strict, less descriptive, and require arguments to be passed in a specific order, otherwise you’ll get an ArgumentError. There are three types of positional arguments you can use:

    • req: A required parameter with no default value.

    • opt: An optional parameter that always has a default value and should only be defined after a required positional parameter.

    • rest: Optional parameters — also known as single splats (*) — which can be any number of parameters after your optional positionals. ⚠️ Heavy use of this parameter is considered a code smell because, when unchecked, can get quite large so use with care or avoid altogether.

  • Keywords: Keyword parameters are more descriptive than positional parameters and have a similar breakdown to positional parameters:

    • keyreq: A required parameter with no default value.

    • key: An optional parameter that always has a default value and should only be defined after a required keyword parameter.

    • keyrest: Optional parameters — also known as double splats (**) — which can be any number of parameters that come after your optional keywords. ⚠️ Heavy use of this parameter is considered a code smell because, when unchecked, can get quite large so use with care or avoid altogether.

  • Block: You can only define a single block parameter and must be in the last position. In our case, the block parameter is explicitly named seven even though most people tend to use block as the parameter name. A deeper dive into blocks is out of scope for this article but you can learn more about them in my function composition article. In the meantime, here’s a quick explanation:

    • Implicit: All methods can implicitly accept a block. Example: demo { "Example." }. Implicit blocks make for an elegant design when coupled with block_given? in order to yield to the block passed to your method.

    • Explicit: If you want to explicitly reference a block, then you must name it. Example: def demo(&block) = super. Explicit blocks are great when you need to use the block directly without the need to check if a block has been implicitly defined.

    • Anonymous: Anonymous blocks were first introduced in Ruby 3.1.0 and are great for forwarding to another method without needing to name them first. Example: def demo(&) = super.

Forwards

Argument forwarding was first introduced in Ruby 2.7.0. The syntax only requires three dots (ellipses) to use. Example:

def demo(...) = super

ap method(:demo).parameters

# [
#   [
#     :rest,
#     :*
#   ],
#   [
#     :keyrest,
#     :**
#   ],
#   [
#     :block,
#     :&
#   ]
# ]

What’s important to highlight is that this resolves to the following:

  • Uses :rest as the kind and :* as the name (i.e. anonymous single splat).

  • Uses :keyrest as the kind and :** as the name (i.e. anonymous double splat).

  • Uses :block as the kind and :& as the name (i.e. anonymous block).

Argument forwarding was enhanced with leading arguments in Ruby 3.0.0. Example:

def demo(one, ...) = super

ap method(:demo).parameters

# [
#   [
#     :req,
#     :one
#   ],
#   [
#     :rest,
#     :*
#   ],
#   [
#     :keyrest,
#     :**
#   ],
#   [
#     :block,
#     :&
#   ]
# ]

While the above is a simple example that only uses a leading required positional parameter, you can definitely lead with both required and optional positional arguments as well. Keywords and blocks are not allowed, though, and will cause a syntax error.

The takeaway is the kinds and names of these parameters are important because they allow you to pattern match and dynamically distinguish between a method using forwarding, anonymous splats/blocks, or named parameters.

Splats

There are three kinds of parameter splats: forwards, anonymous, or named. You’ve already seen how forwarding works and to illustrate further — with the block parameter included for completeness — the following table provides a visual breakdown of the different kinds of splats in Ruby 3.2.0 and higher:

Kind Method Parameters

Forwards

def demo(...) = super

[[:rest, :*], [:keyrest, :**], [:block, :&]]

Anonymous

def demo(*, **, &) = super

[[:rest, :*], [:keyrest, :**], [:block, :&]]

Named

def demo(*positionals, **keywords, &block) = super

[[:rest, :positionals], [:keyrest, :keywords], [:block, :block]]

Prior to the release of Ruby 3.2.0, forwarding of bare splats wasn’t supported which meant a method definition of def demo(*, **) = super would result in the following parameters: [[:rest], [:keyrest]]. With Ruby 3.2.0 and higher, using forwarded or anonymous parameters always results in identical parameters as emphasized above: [[:rest, :*], [:keyrest, :**]].

We’ll dive into the details of each kind of parameter next.

Anonymous

Anonymous splats are denoted by use of single (*) and/or double (**) splats. The single splat is an Array while the double splat is a Hash. Here’s an example:

def demo(*, **) = super

ap method(:demo).parameters

# [
#   [
#     :rest,
#     :*
#   ],
#   [
#     :keyrest,
#     :**
#   ]
# ]

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 a single or double 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.

💡 Despite anonymous splats being…​well…​anonymous, you can debug them by splatting them into an empty array or hash accordingly. Don’t use this in production, though. Example:

def demo(*, **) = puts "Positionals: #{[*]}, Keywords: #{{**}}."

demo 1, 2, c: 3, d: 4

# Positionals: [1, 2], Keywords: {:c=>3, :d=>4}.

⚠️ There is subtle — and inconsistent — behavior between single and double splats that is worth knowing about. For instance, the Ruby parser will explicitly message #to_a on a single splat while implicitly messaging #to_hash on a double splat. The fact that single and double splats don’t have the same behavior — in other words, always using explicit conversion — is a bug that is being addressed in this issue.

Named

In contrast to anonymous splats, we can use named splats as well. Example:

def demo(*positionals, **keywords) = super

ap method(:demo).parameters

# [
#   [
#     :rest,
#     :positionals
#   ],
#   [
#     :keyrest,
#     :keywords
#   ]
# ]

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.

Post

The post parameter is more obscure, rare, and a bit awkward when you see it in the wild. A post parameter is a required positional parameter that comes after an optional positional parameter. Example:

def demo(one, two = 2, three) = super

ap method(:demo).parameters

# [
#   [
#     :req,
#     :one
#   ],
#   [
#     :opt,
#     :two
#   ],
#   [
#     :req,
#     :three
#   ]
# ]

In very rare cases, post parameters can be useful but, in general, suffers from the following:

  • They are harder to read, surprising, and uncommon to see in actual code.

  • 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.

No Keywords

This keyword parameter — though, rarely used — comes in handy when you don’t want to support any keywords. Example:

def demo(**nil) = super

ap method(:demo).parameters

# [
#   [
#     :nokey
#   ]
# ]

Generally, you combine the nokey parameter with positional parameters. Example:

def demo(one, two = 2, **nil) = super

ap method(:demo).parameters

# [
#   [
#     :req,
#     :one
#   ],
#   [
#     :opt,
#     :two
#   ],
#   [
#     :nokey
#   ]
# ]

Once the nokey parameter is defined, you can’t provide any required or optional parameters (blocks are always allowed, though) as the following syntax is invalid:

def demo(**nil, one:, two: 2) = super
# unexpected label, expecting & or '&' (SyntaxError)

Keyword Overrides

Ruby consists of 40 language keywords. In rare situations, you might want to use one of those keywords as a method parameter but this will cause a syntax error. For example, the following errors because both the begin and end keywords conflict with the built-in language keywords:

def demo(begin: 1, end: 10) = puts "Begin: #{begin}, End: #{end}."
demo  # syntax error, unexpected '}' (SyntaxError)

You can workaround this situation by using binding. Here’s how to rewrite the above to solve the problem:

def demo begin: 1, end: 10
  puts "Begin: #{binding.local_variable_get(:begin)}, End: #{binding.local_variable_get(:end)}."
end

demo  # "Begin: 1, End: 10."

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 definitions in rare situations but you’d only want to reach for this when all other options have been exhausted.

Parenthesis

As an obscure syntactical oddity, you can wrap a required positional parameter in parenthesis as a way to bypass being able to pass a tuple (or larger) as a destructured required parameter. The inner parenthesis is critical to making this work and only works for required parameters. Example:

def demo((one, two)) = super

ap method(:demo).parameters

# [
#   [
#     :req
#   ]
# ]

Notice that you only get :req as the kind but no name. In truth, you have two parameter names (i.e. one and two) — each destructured — but the #parameters method can’t show you that since it has no way to represent multiple names for a kind. Here’s a more complete illustration:

def demo((one, two)) = puts "One: #{one}, Two: #{two}"
demo [1, 2]  # One: 1, Two: 2

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(...)
end

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(*)
end

# Keyword Only. Parameters: `[[:keyrest, :**]]`.
Demo = Struct.new :one, :two do
  def self.for(**) = new(**)
end

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}"
end

# 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)
end

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.

Arguments

To understand arguments, we’ll need to a more…​introspective…​implementation:

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

Keep this implementation in mind or scroll back, when needed, to reread since this implementation will be referred to repeatedly from this point.

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:

  1. req: a: This positional argument was required so we had to pass in a value (i.e. :a).

  2. opt: b: This positional argument was optional so the default value of :b was answered back.

  3. 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.

  4. keyreq: d: This keyword argument was required so we had to pass in a value (i.e. :d).

  5. key: e: This keyword argument was optional so the default value of :e was answered back.

  6. 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.

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

To contrast the minimal set of arguments passed to the .demo method, we can also pass in a maximum — loosely speaking — set of arguments. Example:

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:

  1. req: a

  2. opt: b

  3. rest: [:y, :z]: Any number of optional positional arguments could have been supplied here but only :y and :z were used in this case.

  4. keyreq: d

  5. key: e

  6. 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.

  7. block: #<Proc:0x000000010d1cbc78>: Since we passed in an explicit block, you can see it’s pass through and printed out as well.

With the basic use of arguments in mind, we can now expand into more advanced usage.

Splats

Argument splats — as mentioned earlier when first discussing parameters — can be categorized as follows:

  • Single (array): Can be bare (i.e. *) or named (example: *positionals).

  • Double (hash): Can be bare (i.e. **) or named (example: **keywords).

  • Block (proc): Can be bare (i.e. &) or named (example: &block).

When these are arguments are passed in, they will be automatically coerced by Ruby. For simplicity, I’m using named arguments but the same applies for bare arguments:

  • 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.

Message Delegation

At this point, we are now primed to discuss message delegation which is where parameter and argument compatibility gets most interesting. To set the stage, we’ll expand upon our earlier implementation by using the following snippet of code:

#! /usr/bin/env ruby
# 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"
end

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

class Exampler
  def initialize operation, method, marameters: Marameters
    @operation = operation
    @method = method
    @marameters = marameters
  end

  def first_example(...)
    operation.public_send(method, ...)
  end

  def second_example(*, **, &)
    operation.public_send(method, *, **, &)
  end

  def third_example *positionals, **keywords, &block
    operation.public_send method, *positionals, **keywords, &block
  end

  def fourth_example arguments
    positionals, keywords, block = arguments
    operation.public_send method, *positionals, **keywords, &block
  end

  def fifth_example result
    result.fmap do |positionals, keywords, block|
      operation.public_send method, *positionals, **keywords, &block
    end
  end

  def sixth_example arguments
    marameters.categorize(operation.method(method).parameters, arguments)
              .then do |splat|
                operation.public_send method, *splat.positionals, **splat.keywords, &splat.block
              end
  end

  private

  attr_reader :operation, :method, :marameters
end

The major changes are that:

  • The above is written as a Bundler Inline script so you can tinker with the code locally.

  • 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
end

# 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
end

# Yields: `demo': missing keyword: :four (ArgumentError)

Destructuring

Last, but not least, is argument destructuring. Both the third and fourth example methods tackle this in similar ways by wrapping of all arguments within a single array. Consider the following:

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__
end

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 so will now call attention to it. 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.

Conclusion

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!