:toc: macro :toclevels: 5 :figure-caption!:
:amazing_print_link: link:https://github.com/amazing-print/amazing_print[Amazing Print] :article_link: link:https://alchemists.io/articles/ruby_method_parameters_and_arguments[method parameters and arguments]
= 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:
def demo one, two: nil
.demo 1, two: 2
.This gem will help you debug methods or aid your workflow when metaprogramming -- as used in the link:https://alchemists.io/projects/infusible[Infusible] gem -- when architecting more sophisticated applications.
toc::[]
== Features
== Requirements
. link:https://www.ruby-lang.org[Ruby]. . A solid understanding of {article_link}.
== Setup
To install with security, run:
To install without security, run:
You can also add the gem directly to your project:
Once the gem is installed, you only need to require it:
== 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:
def demo(one, two = 2, three: 3) = puts "One: #{one}, Two: #{two}, Three: #{three}"
parameters = method(:demo).parameters arguments = %w[one two]
Marameters.categorize parameters, arguments
Marameters.of self, :demo # []
probe = Marameters.for parameters probe.to_a # [[:req, :one], [:opt, :two], [:key, :three]] probe.positionals # [:one, :two] probe.keywords # [:three] probe.block # nil
Marameters.signature({req: :one, opt: [:two, 2], key: [:three, 3]}).to_s
Read on to learn more about the details on how each of these methods work and the objects they wrap.
=== Probe
The 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
You can then probe the #all
method's parameters as follows:
probe = Marameters::Probe.new Demo.instance_method(:all).parameters
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
=== Categorizer
The 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_link} (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::Categorizer.new(Demo.method(:test).parameters) .call(arguments).then do |splat| ap splat puts Demo.test(*splat.positionals, **splat.keywords, &splat.block) end end end
Inspector.call [1, nil, nil, {four: 4}]
When we step through the above implementation and output, we see the following unfold:
. The Demo
module allows us to define a maximum set of parameters and then print the arguments received for inspection purposes.
. The Inspector
module provides a wrapper around the Categorizer
so we can conveniently pass in different arguments for experimentation purposes.
. We pass in our arguments to Inspector.call
where nil
is used for optional arguments and hashes for keyword arguments.
. Once inside Inspector.call
, the Categorizer
is initialized with the Demo.test
method parameters.
. Then the splat
(i.e. Struct) is printed out so you can see the categorized positional, keyword, and block arguments.
. 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]
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:
With {amazing_print_link}, we can print out this information:
...which can be further illustrated by this comparison table:
[options="header"]
|===
| 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 that:
nil
to fill an optional argument when you don't need it.:rest
(single splat) argument must be an array or nil
if not present because even though it is optional, it is still positional.: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. Having to remember to wrap your argument in an array each time can get tedious so when only a single argument is supplied, the categorizer will automatically cast the argument as an array. A good example of this use case is when using structs. Example:
url = Struct.new :label, :url, keyword_init: true
Marameters.categorize(url.method(:new).parameters, {label: "Eaxmple", url: "https://example.com"}) .then { |splat| url.new(*splat.positionals, **splat.keywords) }
For further details, please refer back to my {article_link} article mentioned in the Requirements section.
=== Signature
The signature class is the inverse of the probe class 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
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):
puts Marameters::Signature.new({key: :demo})
puts Marameters::Signature.new({key: [:demo, nil]})
puts Marameters::Signature.new({key: [:demo, "test"]})
puts Marameters::Signature.new({key: [:demo, :test]})
puts Marameters::Signature.new({key: [: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:
*
) signifies you want an object to be passed through without further processing whileObject.new
#<Object:0x0000000107df4028>
) which is not what you want until much later when your method isWhen 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
puts Example.say "Hello"
== Development
To contribute, run:
You can also use the IRB console for direct access to all objects:
== Tests
To test, run:
== link:https://alchemists.io/policies/license[License]
== link:https://alchemists.io/policies/security[Security]
== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
== link:https://alchemists.io/policies/contributions[Contributions]
== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]
== link:https://alchemists.io/projects/marameters/versions[Versions]
== link:https://alchemists.io/community[Community]
== Credits