Search code examples
rubydouble-splat

What does the double-splat do in a method call?


While preparing for the Ruby Association Certified Ruby Programmer Exam, I was solving the prep test and came upon this scenario:

def add(x:, y:, **params)
  z = x + y

  params[:round] ? z.round : z
end


p add(x: 3, y: 4) #=> 7 // no surprise here
p add(x: 3.75, y: 3, round: true) #=> 7 // makes total sense
options = {:round => true}; p add(x: 3.75, y: 3, **options) #=> 7 // huh?

Now, I know how the double-splat can be used in converting params in an argument to a hash, e.g.:

def splat_me(a, *b, **c)
  puts "a = #{a.inspect}"
  puts "b = #{b.inspect}"
  puts "c = #{c.inspect}"
end

splat_me(1, 2, 3, 4, a: 'hello', b: 'world')

#=> a = 1
#=> b = [2, 3, 4]
#=> c = {:a=>"hello", :b=>"world"}

However, I also know that you can't double-splat randomly.

options = {:round => true}
**options

#=> SyntaxError: (irb):44: syntax error, unexpected **arg
#=> **options
#=>   ^

Question:

What is the use of the double-splat (**) in method calls (not definitions)?

Plainly put, when is this:

options = {:round => true}; p add(x: 3.75, y: 3, **options)

Better than this:

options = {:round => true}; p add(x: 3.75, y: 3, options)

Edit: Testing the usefulness of the double-splat (none found)

Args are the same with or without it.

def splat_it(**params)
  params
end

opts = {
  one: 1,
  two: 2,
  three: 3
}

a = splat_it(opts)   #=> {:one=>1, :two=>2, :three=>3}
b = splat_it(**opts) #=> {:one=>1, :two=>2, :three=>3}

a.eql? b # => true

I mean, you can even pass a hash to a method defined with keyword params without a problem, and it'll intelligently assign the appropriate keywords:

def splat_it(one:, two:, three:)
  puts "one   = #{one}"
  puts "two   = #{two}"
  puts "three = #{three}"
end

opts = {
  one: 1,
  two: 2,
  three: 3
}

a = splat_it(opts)   #=> {:one=>1, :two=>2, :three=>3}
#=> one   = 1
#=> two   = 2
#=> three = 3

b = splat_it(**opts) #=> {:one=>1, :two=>2, :three=>3}
#=> one   = 1
#=> two   = 2
#=> three = 3

Double splat on a random class with the appropriate to_h and to_hash methods doesn't do anything that can't be done without it:

Person = Struct.new(:name, :age)

Person.class_eval do
  def to_h
    {name: name, age: age}
  end

  alias_method :to_hash, :to_h
end

bob = Person.new('Bob', 15)

p bob.to_h #=> {:name=>"Bob", :age=>15}


def splat_it(**params)
  params
end

splat_it(**bob) # => {:name=>"Bob", :age=>15}
splat_it(bob)   # => {:name=>"Bob", :age=>15}

Solution

  • One might need to destructure the input parameters. In such a case simple hash won’t work:

    params = {foo: 42, bar: :baz}
    def t1(foo:, **params); puts params.inspect; end
    #⇒ :t1
    def t2(foo:, params); puts params.inspect; end 
    #⇒ SyntaxError: unexpected tIDENTIFIER
    def t2(params, foo:); puts params.inspect; end
    #⇒ :t2
    

    Now let’s test it:

    t1 params
    #⇒ {:bar=>:baz}
    t2 params
    #⇒ ArgumentError: missing keyword: foo
    t2 **params
    #⇒ ArgumentError: missing keyword: foo
    

    That said, double splat allows transparent arguments destructuring.

    If one is curious why it might be useful, foo in the example above is made a mandatory parameter in a call to the method within this syntax.


    Unsplatting parameters in call to function is allowed as a sort of sanity type check to assure that all the keys are symbols:

    h1 = {foo: 42}
    h2 = {'foo' => 42}
    def m(p); puts p.inspect; end
    m **h1
    #⇒ {:foo=>42}
    m **h2
    #⇒ TypeError: wrong argument type String (expected Symbol)