The letter A styled as Alchemists logo. lchemists Syndication Icon

Putin's War on Ukraine - Watch President Zelenskyy's speech and help Ukraine fight against the senseless cruelty of a dictator!

Published March 6, 2022 Updated May 7, 2022
Marameters Icon

Marameters

0.4.0

Marameters is a portmanteau (i.e. [m]ethod + p[arameters] = marameters) which is designed to provide additional insight and diagnostics to 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 — more importantly — aid your workflow when metaprogramming, as used in the Auto Injector gem, when architecting more sophisticated applications.

Features

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

Requirements

  1. Ruby.

Setup

To install, run:

gem install marameters

Add the following to your Gemfile file:

gem "marameters"

Usage

There are two main objects you’ll want to interact with:

  • Marameters::Probe: Allows you to analyze a method’s parameters.

  • Marameters::Signature: Allows you to dynamically build a method signature from raw parameters.

Both of these objects are meant to serve as building blocks to more complex architectures.

Probe

To understand how to analyze a method’s parameters, consider the following demonstration class:

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::Probe.new Demo.instance_method(:all).parameters

probe.block                # :seven
probe.block?               # true
probe.empty?               # false
probe.keywords             # [:four, :five]
probe.keywords?            # true
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.splats               # [:three, :six]
probe.splats?              # true
probe.to_a                 # [[:req, :one], [:opt, :two], [:rest, :three], [:keyreq, :four], [:key, :five], [:keyrest, :six], [:block, :seven]]
probe.to_h                 # {:req=>:one, :opt=>:two, :rest=>:three, :keyreq=>:four, :key=>:five, :keyrest=>:six, :block=>:seven}

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

probe = Marameters::Probe.new Demo.instance_method(:none).parameters

probe.block                # nil
probe.block?               # false
probe.empty?               # true
probe.keywords             # []
probe.keywords?            # false
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.splats               # []
probe.splats?              # false
probe.to_a                 # []
probe.to_h                 # {}

Signature

The signature class is the opposite of the probe in that you want to feed it parameters for turning into a method signature. This is useful when dynamically building method signatures or using the same signature when metaprogramming multiple methods.

The following demonstrates how you might construct a method signature with all possible parameters:

signature = Marameters::Signature.new(
  {
    req: :one,
    opt: [:two, 2],
    rest: :three,
    keyreq: :four,
    key: [:five, 5],
    keyrest: :six,
    block: :seven
  }
)

puts signature
# one, two = 2, *three, four:, five: 5, **six, &seven

You’ll notice that the parameters are a hash and some values can be tuples. The reason is that it’s easier to write a hash than a double nested array as normally produced by the probe or directly from Method#parameters. The optional positional and keyword parameters use tuples because you might want to supply a default value and this provides a way for you to do that with minimal syntax. This can be demonstrated further by using optional keywords (same applies for optional positionals):

# With no default
puts Marameters::Signature.new({key: :demo})
# demo: nil

# With explicit nil as default
puts Marameters::Signature.new({key: [:demo, nil]})
# demo: nil

# With string as default
puts Marameters::Signature.new({key: [:demo, "test"]})
# demo: "test"

# With symbol as default
puts Marameters::Signature.new({key: [:demo, :test]})
# demo: :test

# With object(dependency) as default
puts Marameters::Signature.new({key: [:demo, "*Object.new"]})
# demo: Object.new

In the case of object dependencies, you need to wrap these in a string and prefix them with a star (*) so the signature builder won’t confuse them as normal strings. There are two reasons why this is important:

  • The star (*) signifies you want an object to be passed through without further processing while also not being confused as a normal string.

  • Objects wrapped as strings allows your dependency to be lazy loaded. Otherwise, if Object.new was pass in directly, you’d be passing the evaluated instance (i.e. #<Object:0x0000000107df4028>) which is not what you want until much later when your method is defined.

When you put all of this together, you can dynamically build a method as follows:

signature = Marameters::Signature.new({opt: [:text, "This is a test."]})

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

puts Example.say
# This is a test.

puts Example.say "Hello"
# Hello

Development

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

bin/console

Tests

To test, run:

bundle exec rake

Credits