What Does a Double * (Splat) Operator Do

What does a double * (splat) operator do

Ruby 2.0 introduced keyword arguments, and ** acts like *, but for keyword arguments. It returns a Hash with key / value pairs.

For this code:

def foo(a, *b, **c)
[a, b, c]
end

Here's a demo:

> foo 10
=> [10, [], {}]
> foo 10, 20, 30
=> [10, [20, 30], {}]
> foo 10, 20, 30, d: 40, e: 50
=> [10, [20, 30], {:d=>40, :e=>50}]
> foo 10, d: 40, e: 50
=> [10, [], {:d=>40, :e=>50}]

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

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)

What does double splat (**) argument mean in this code example and why use it?

what does ** mean in the block above?

It's a kwsplat, but it's not assigned a name. So this method will accept arbitrary set of keyword arguments and ignore all but :fragment.

why use this syntax?

To ignore arguments you're not interested in.


A little demo

class Person
attr_reader :name, :age

def initialize(name:, age:)
@name = name
@age = age
end

def description
"name: #{name}, age: #{age}"
end
end

class Rapper < Person
def initialize(name:, **)
name = "Lil #{name}" # amend one argument
super # send name and the rest (however many there are) to super
end
end

Person.new(name: 'John', age: 25).description # => "name: John, age: 25"
Rapper.new(name: 'John', age: 25).description # => "name: Lil John, age: 25"

What is the point of using Ruby's double-splat (`**`) in method calls?

The example using a single argument is the degenerate case.

Looking at a nontrivial case, you can quickly see the advantage of having the new ** operator:

def foo (args)
return args
end

h1 = { b: 2 }
h2 = { c: 3 }

foo(a: 1, **h1) # => {:a=>1, :b=>2}
foo(a: 1, **h1, **h2) # => {:a=>1, :b=>2, :c=>3}
foo(a: 1, h1) # Syntax Error: syntax error, unexpected ')', expecting =>
foo(h1, h2) # ArgumentError: wrong number of arguments (2 for 1)

Using the ** operator allows us to merge existing hashes together in the command line, along with literal key-value arguments. (The same is true of using * with argument arrays, of course.)

Sadly, you have to be careful with this behavior, depending on what version of Ruby you're using. In Ruby 2.1.1, at least, there was a bug where the splatted hash would be destructively modified, though it's since been patched.

Ruby Splat operator in method definition takes more memory

Let's examine the two arrays more closely:

require 'objspace'

def with_splat(*methods)
ObjectSpace.dump(methods, output: open('with_splat.json', 'w'))
end

def without_splat(methods)
ObjectSpace.dump(methods, output: open('without_splat.json', 'w'))
end

with_splat(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o)
without_splat([:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o])

ObjectSpace.dump_all(output: open('all_objects.json', 'w'))

The script generates 3 files:

  • with_splat.json contains data about the splatted array
  • without_splat.json contain data about the non splatted array
  • all_objects.json contains data about all objects (that's a lot!)

with_splat.json: (formatted)

{
"address": "0x7feb941289a0",
"type": "ARRAY",
"class": "0x7feb940972c0",
"length": 15,
"memsize": 160,
"flags": {
"wb_protected": true
}
}

without_splat.json: (formatted)

{
"address": "0x7feb941287e8",
"type": "ARRAY",
"class": "0x7feb940972c0",
"length": 15,
"shared": true,
"references": [
"0x7feb941328d8"
],
"memsize": 40,
"flags": {
"wb_protected": true
}
}

As you can see, the latter array does consume less memory (40 vs 160), but it also has "shared": true set and it references another object at memory address 0x7feb941328d8.

Let's find that object in all_objects.json via jq:

$ jq 'select(.address == "0x7feb941328d8")' all_objects.json
{
"address": "0x7feb941328d8",
"type": "ARRAY",
"frozen": true,
"length": 15,
"memsize": 160,
"flags": {
"wb_protected": true
}
}

And that's the actual array with the very same memsize as the first array above.

Note that this array has "frozen": true set. I assume that Ruby creates these frozen arrays when it encounters an array literal. It can then create cheap(er) shared arrays upon evaluation.

What does a splat operator do when it has no variable name?

Typically a splat like this is used to specify arguments that are not used by the method but that are used by the corresponding method in a superclass. Here's an example:

class Child < Parent
def do_something(*)
# Do something
super
end
end

This says, call this method in the super class, passing it all the parameters that were given to the original method.

source: Programming ruby 1.9 (Dave Thomas)

Double splat and default value in method profile

If the input to the method MUST be options hash, then, use double splat operator **.

Using options = {} only declares the default value to be empty hash, however, it does not necessarily guarantee that caller will pass hash - she may pass non-hash values and nil.

If the function was implemented using double splat (**) - as evident in examples you have provided - then non-hash and nil values will not be accepted and will be reported as error.

Using splat operator with when

But the splat operator is about assignment, not comparison.

In this case, * converts an array into an argument list:

when *[2, 3, 4]

is equivalent to:

when 2, 3, 4

Just like in a method call:

foo(*[2, 3, 4])

is equivalent to:

foo(2, 3, 4)

Make Ruby object respond to double splat operator **

You can implement to_hash: (or define it as an alias for to_h)

class MyClass
def to_hash
{ a: 1, b: 2 }
end
end

def foo(**kwargs)
p kwargs: kwargs
end

foo(MyClass.new)
#=> {:kwargs=>{:a=>1, :b=>2}}


Related Topics



Leave a reply



Submit