Project

marameters

0.0
The project is in a healthy, maintained state
A dynamic method parameter enhancer.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

~> 2.7
 Project Readme

Marameters

Marameters is a portmanteau (i.e. [m]ethod + p[arameters] = marameters) which is designed to provide additional insight and diagnostics for method parameters. For context, the difference between a method’s parameters and arguments is:

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

This gem will help you debug methods or aid your workflow when metaprogramming — as used in the Infusible gem — when architecting more sophisticated applications.

Table of Contents
  • Features
  • Requirements
  • Setup
  • Usage
    • Constants
    • Probe
    • Categorize
    • Signature
      • Keys
      • Values
      • Argument Forwarding
      • Bare
      • Inheritance
  • Development
  • Tests
  • License
  • Security
  • Code of Conduct
  • Contributions
  • Developer Certificate of Origin
  • Versions
  • Community
  • Credits

Features

  • Provides specialized objects for keyword, positional, and block parameters.

Requirements

  1. Ruby.

  2. A solid understanding of method parameters and arguments.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install marameters --trust-policy HighSecurity

To install without security, run:

gem install marameters

You can also add the gem directly to your project:

bundle add marameters

Once the gem is installed, you only need to require it:

require "marameters"

Usage

At a high level, you can use Marameters as a single Object API for accessing all capabilities provided by this gem. Here’s an overview:

Setup

def demo(one, two = 2, three: 3) = puts "One: #{one}, Two: #{two}, Three: #{three}"

parameters = method(:demo).parameters
arguments = %w[one two]

Categorize

Marameters.categorize parameters, arguments
# #<struct Marameters::Models::Forward positionals=["one", "two"], keywords={}, block=nil>

Probe

Marameters.of self, :demo            # []

probe = Marameters.for parameters
probe.positionals                    # [:one, :two]
probe.keywords                       # [:three]
probe.to_a                           # [[:req, :one], [:opt, :two], [:key, :three]]

Signature

Marameters.signature([%i[req one], [:opt, :two, 2], [:key, :three, 3]]).to_s
# "one, two = 2, three: 3"

Constants

The KINDS constant allows you to know the kinds of parameters allowed:

Marameters::KINDS
# [
#   :req,
#   :opt,
#   :rest,
#   :nokey,
#   :keyreq,
#   :key,
#   :keyrest,
#   :block
# ]

Probe

The probe (Marameters::Probe) allows you to analyze a method’s parameters. To understand how, consider the following:

class Demo
  def initialize logger: Logger.new(STDOUT)
    @logger = logger
  end

  def all one, two = nil, *three, four:, five: nil, **six, &seven
    logger.debug [one, two, three, four, five, six, seven]
  end

  def none = logger.debug "Nothing to see here."

  private

  attr_reader :logger
end

You can then probe the #all method’s parameters as follows:

probe = Marameters.for Demo.instance_method(:all).parameters

probe.deconstruct                      # (same as to_a, see below)
probe.empty?                           # false
probe.include? %i[req one]             # true
probe.keywords                         # [:four, :five]
probe.keywords?                        # true
probe.keywords_for :four, four: :demo  # {four: :demo}
probe.kind?(:keyrest)                  # true

probe.kinds
# [:req, :opt, :rest, :keyreq, :key, :keyrest, :block]

probe.name?(:three)                    # true

probe.names
# [:one, :two, :three, :four, :five, :six, :seven]

probe.only_bare_splats?                # false
probe.only_double_splats?              # false
probe.only_single_splats?              # false
probe.positionals                      # [:one, :two]
probe.positionals?                     # true
probe.positionals_and_maybe_keywords?  # true

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

In contrast to the above, we can probe the #none method which has no parameters for a completely different result:

probe = Marameters.for Demo.instance_method(:none).parameters

probe.deconstruct                      # (same as to_a, see below)
probe.empty?                           # true
probe.include? %i[req one]             # false
probe.keywords                         # []
probe.keywords?                        # false
probe.keywords_for :four, four: :demo  # {}
probe.kind?(:req)                      # true
probe.kinds                            # []
probe.name?(:three)                    # false
probe.names                            # []
probe.only_bare_splats?                # false
probe.only_double_splats?              # false
probe.only_single_splats?              # false
probe.positionals                      # []
probe.positionals?                     # false
probe.positionals_and_maybe_keywords?  # false
probe.to_a                             # []

The #keywords_for method might need additional explaining because it’s meant for selecting keywords which adhere to either of the following criteria:

  • The given keys don’t match any key in the given attributes.

  • The given keys match the parameter keywords.

module Demo
  def self.keywords(four:, five: 5, **six) = puts "Four: #{four}, Five: #{five}, Six: #{six}"
end

probe = Marameters.for Demo.method(:keywords).parameters

probe.keywords_for :a, a: 1, four: 4         # {four: 4}
probe.keywords_for :four, a: 1               # {a: 1}
probe.keywords_for :a, four: 4, five: :five  # {four: 4, five: :five}
probe.keywords_for :a, six: {name: :test}    # {six: {name: :test}}

This useful in gems, like Infusible, when determining which keyword arguments to pass up to the superclass.

Categorize

Categorization (Marameters::Categorizer) allows you to dynamically build positional, keyword, and block arguments for message passing. This is most valuable when you know the object and method while needing to align the arguments in the right order. Here’s a demonstration where Amazing Print (i.e. ap) is used to format the output:

function = proc { "test" }

module Demo
  def self.test one, two = nil, *three, four:, five: nil, **six, &seven
    puts "The .#{__method__} method received the following arguments:\n"

    [one, two, three, four, five, six, seven].each.with_index 1 do |argument, index|
      puts "#{index}. #{argument.inspect}"
    end

    puts
  end
end

module Inspector
  def self.call arguments
    Marameters.categorize(Demo.method(:test).parameters, arguments)
              .then do |record|
                ap record
                puts
                Demo.test(*record.positionals, **record.keywords, &record.block)
              end
  end
end

Inspector.call [1, nil, nil, {four: 4}]

# #<Struct:Marameters::Models::Forward:0x00021930
#   block = nil,
#   keywords = {
#     :four => 4
#   },
#   positionals = [
#     1,
#     nil
#   ]
# >
#
# The .test method received the following arguments:
# 1. 1
# 2. nil
# 3. []
# 4. 4
# 5. nil
# 6. {}
# 7. nil

When we step through the above implementation and output, we see the following unfold:

  1. The Demo module allows us to define a maximum set of parameters and then print the arguments received for inspection purposes.

  2. The Inspector module provides a wrapper around the categorization so we can conveniently pass in different arguments for experimentation purposes.

  3. We pass in our arguments to Inspector.call where nil is used for optional arguments and hashes for keyword arguments.

  4. Once inside Inspector.call, the Categorizer is initialized with the Demo.test method parameters.

  5. Then the splat (i.e. Struct) is printed out so you can see the categorized positional, keyword, and block arguments.

  6. Finally, Demo.test method is called with the splatted arguments.

The above example satisfies the minimum required arguments but if we pass in the maximum arguments — loosely speaking — we see more detail:

Inspector.call [1, 2, [98, 99], {four: 4}, {five: 5}, {twenty: 20, thirty: 30}, function]

# Output

# #<Struct:Marameters::Models::Forward:0x00029cc0
#   block = #<Proc:0x000000010a88cec0 (irb):1>,
#   keywords = {
#       :four => 4,
#       :five => 5,
#     :twenty => 20,
#     :thirty => 30
#   },
#   positionals = [
#     1,
#     2,
#     98,
#     99
#   ]
# >
#
# The .test method received the following arguments:
# 1. 1
# 2. 2
# 3. [98, 99]
# 4. 4
# 5. 5
# 6. {:twenty=>20, :thirty=>30}
# 7. #<Proc:0x000000010a88cec0 (irb):1>

Once again, it is important to keep in mind that the argument positions must align with the parameter positions since the parameters are an array of elements too. For illustration purposes — using the above example — we can compare the parameters to the arguments as follows:

parameters = Demo.method(:test).parameters
arguments = [1, 2, [98, 99], {four: 4}, {five: 5}, {twenty: 20, thirty: 30}, function]

With Amazing Print, we can print out this information:

ap parameters
ap arguments

…​which can be further illustrated by this comparison table:

Parameter Argument

%i[reg one]

1

%i[opt two]

2

%i[rest three]

[98, 99]

%i[keyreq four]

{four: 4}

%i[key five]

{five: 5}

%i[keyrest six]

{twenty: 20, thirty: 30}

%i[block seven]

#<Proc:0x0000000108edc778>

This also means:

  • All positions must be filled if you want to supply arguments beyond the first couple of positions because everything is positional due to the nature of how Method#parameters works. Use nil to fill an optional argument when you don’t need it.

  • The :rest (single splat) argument must be an array or nil if not present because even though it is optional, it is still positional.

  • The :keyrest (double splat) argument — much like the :rest argument — must be a hash or nil if not present.

Lastly, in all of the above examples, only an array of arguments has been used but you can pass in a single argument too (i.e. non-array). This is handy for method signatures which have only a single parameter or only use splats.

For C-based primitives, like Struct, Data, etc., you’ll want to provide a conversion method. Example:

url = Struct.new(:label, :url) do
  def self.for(**) = new(**)
end

Marameters.categorize(url.method(:for).parameters, label: "Example", url: "https://example.com")
          .then { |record| url.for(**record.keywords) }

# Yields: #<struct label="Example", url="https://example.com">

For further details, please refer back to my method parameters and arguments article mentioned in the Requirements section.

Signature

The signature (Marameters::Signature) is the opposite of the probe class which allows you to turn a raw array of parameters into a method signature. This is most useful when metaprogramming and needing to dynamically build method signatures. Example:

signature = Marameters.signature [[:opt, :text, "This is a test."]]

Example = Module.new do
  module_eval <<~METHOD, __FILE__, __LINE__ + 1
    def self.say(#{signature}) = text
  METHOD
end

puts Example.say           # "This is a test."
puts Example.say("Hello")  # "Hello"

Keys

The following demonstrates how you can construct a method signature with all possible parameters using the same keys as used by Method#parameters:

signature = Marameters.signature [
  %i[req one],
  %i[opt two],
  %i[rest three],
  %i[keyreq four],
  %i[key five],
  %i[keyrest six],
  %i[block seven]
]

puts signature
# "one, two = nil, *three, four:, five: nil, **six, &seven"

Values

With the above examples, each sub-array uses a simple key/value pair to map the kind of parameter with the corresponding name. You can also provide a third value when needing to provide a default value for optional parameters. Example:

puts Marameters.signature([[:opt, :one, 1], [:key, :two, 2]])
# one = 1, two: 2

This can be demonstrated further by using optional keywords (same applies for optional positionals):

# With implicit nil.
puts Marameters.signature([%i[key demo]])
# "demo: nil"

# With explicit nil.
puts Marameters.signature([[:key, :demo, nil]])
# "demo: nil"

# With any primitive.
puts Marameters.signature([[:key, :demo, :test]])
# "demo: :test"

# With proc (no parameters).
puts Marameters.signature([[:key, :demo, proc { Object.new }]])
# "demo: Object.new"

# With proc (with parameters).
puts Marameters.signature([[:key, :demo, proc { |no| no }]])
# Avoid using parameters for proc defaults. (ArgumentError)

# With lambda.
puts Marameters.signature([[:key, :demo, -> { Object.new }]])
# Use procs instead of lambdas for defaults. (TypeError)

You can use any primitive, custom object, etc. as a default despite the limited examples shown above.

Procs must be used when supplying complex objects as default values. Avoid using parameters when using procs because only the source (body) of your proc will be used as a literal string when building the method signature in order to ensure lazy evaluation.

Lastly, you can use anonymous splats/blocks by only supplying their kind. Example:

puts Marameters.signature([[:rest], [:keyrest], [:block]])
# "*, **, &"

You can supply nil as a second element (i.e. the name) for each kind but that is the equivalent of the above.

Argument Forwarding

Use :all for building a method signature with argument forwarding. Example:

puts Marameters.signature(:all)
# "..."

Use of :all is special in that you must only supply :all with no other keys/values or you’ll get an ArgumentError.

💡 This is only provided for convenience and completeness. In truth, you’re better off writing my_method(...), for example, than using this class.

Bare

Use an empty array when you need a bare method signature. Example:

puts Marameters.signature []
# ""

💡 This is only provided for convenience and completeness. In truth, if you need a bare method, then you don’t need to use this class.

Inheritance

Object/method inheritance is more complicated than building a signature for a single method because you need to blend the super and sub parameters as a unified set of parameters. Additionally, you have to account for the arguments that need to be forwarded to the super method via the super keyword. To aid in this endeavor, the following objects are available to help you build these more complex method parameters and arguments:

  • Marameters::Signatures::Inheritor: Blends super and sub parameters to produce a unified set of parameters you can turn into a method signature.

  • Marameters::Signatures::Super: Blends super and sub parameters to produce arguments for forwarding via the super keyword. This does not support disabled block forwarding (i.e. &nil) since there is no way to determine this from the super and sub parameters alone.

Here’s an example which incorporates both of the above:

module Demo
  def self.parent(one, two = 2, *three, &block) = nil
end

super_parameters = Marameters.for Demo.method(:parent).parameters

sub_parameters = Marameters.for [
  [:opt, :two, 22],
  %i[keyreq four],
  [:key, :five, 5],
  %i[keyrest six]
]

inheritor = Marameters::Signatures::Inheritor.new
forwarder = Marameters::Signatures::Super.new

puts Marameters.signature inheritor.call(super_parameters, sub_parameters)
# "one, two = 22, *three, four:, five: 5, **six, &block"

puts forwarder.call(super_parameters, sub_parameters)
# "one, two, *three, &block"

As you can see, the above combines the parameters of your super method with the parameters of your sub method in order to produce a method signature — with no duplicates — while ensuring you can forward all necessary parameters that the super keyword requires. Defaults, if given, will override previously defined defaults as is identical with standard object inheritance.

Development

To contribute, run:

git clone https://github.com/bkuhlmann/marameters
cd marameters
bin/setup

You can also use the IRB console for direct access to all objects:

bin/console

Tests

To test, run:

bin/rake

Credits