ruby-on-railsruby

Method-arguments: How can one distinguish between a keyword-argument and a hash as last argument?


For example:

class Person < ApplicationRecord
  validates :name, presence: true
end

Is "presence: true" a hash with key "presence" and value "true"? Or is it a keyword-argument?

How does one know, what the second argument is?

How does one handle such cases in general? Or isn't it relevant and I shouldn't bother?


Solution

  • Or isn't it relevant and I shouldn't bother?

    In general, it's not relevant unless you're writing methods that need to distinguish between positional hashes and keyword arguments explicitly. Ruby often treats them similarly, especially if you're just passing them around — but there is a difference under the hood.

    TL;DR

    They’re both just Hashes at runtime, but Ruby differentiates between positional hashes and keyword arguments in method signatures. So if you're inspecting method arity, introspecting code, or using advanced metaprogramming, it is relevant — otherwise, you probably don’t need to worry about it.

    In case you're wondering what's going on internally

    (Tested in Ruby 3.0) Let’s look at a small example:

    class TestArgs
      def call(opts = {}, opts_kwargs: {})
        puts "Keyword argument: #{opts_kwargs}"
        puts "Options: #{opts.inspect}"
    
        puts "opts_kwargs.class => #{opts_kwargs.class}"
        puts "opts.class => #{opts.class}"
      end
    end
    
    test_args = TestArgs.new
    test_args.call({ keyword_argument: 'bar' }, opts_kwargs: { keyword_argument: 'baz' })
    
    puts TestArgs.instance_method(:call).parameters.inspect
    

    Output

    Keyword argument: {:keyword_argument=>"baz"}
    Options: {:keyword_argument=>"bar"}
    
    opts_kwargs.class => Hash
    opts.class => Hash
    
    [[:opt, :opts], [:key, :opts_kwargs]]
    

    What's the takeaway?

    Both opts and opts_kwargs are Hash objects — so at runtime, they're indistinguishable by class.

    But Method#parameters reveals how Ruby interprets them at the method definition level:

    So while they may look and behave similarly, Ruby keeps track of them differently in the method signature.