Silence is Foo Mental notes on Ruby, Git, Rails and whatever geeky thing

7Aug/10Off

[Ruby Idioms] What is the splat/unary/asterisk operator useful for?

Long story short: the splat operator is useful to explode and flatten arrays and hashes (enumerables).

Doesn't sound like something powerful, does it?

Code is worth a thousand words.

1. Passing a variable number of arguments to a method


Let's get started with an hypothetical method called ActiveRecord::Base#find. Please, don't care about the functionality but the method signature.


def find(*args)
    ...
    [args, args.class]
end

find 		# => [[], Array]

find(:all) # => [[:all], Array]

find(:all, :select => "name") # => [[:all, :select => "name"], Array]

find(:all, :select => "name", :conditions => [ "age > ?", 21], :limit => 10)

As you can see, the ActiveRecord::Base#find method accept a variable number of arguments, it's even able to be called without arguments, all of this thanks to the asterisk (splat) which initializes "args" as an empty array. Of course, the real ActiveRecord::Base#find method will raise an exception if you call it without arguments, but it's not a Ruby exception, it allows you to call the method without arguments.

Now let's see the ActionView::Helpers::TextHelper#highlight method:

def highlight(text, phrases, *args)
...
end

highlight("lorem ipsum") # => wrong number of arguments (1 for 2)

highlight("lorem ipsum", "ipsum") # => lorem <strong class="highlight">ipsum</strong> dolorem

highlight("lorem ipsum", "ipsum", :highlighter => <b>\1</b>) # => lorem <b>ipsum</b> dolorem

The highlight method uses the splat in the last argument to avoid to put the default highlighter in its signature, this way it's cleaner and it'd accept more options in the future without changing it.

In Ruby 1.8 the splat operator must be used only once per method and it must be used in the last argument, the only exception is when the last argument is a proc, like in the ActionView::Helpers::FormHelper#form_for method:

def form_for(record, *args, proc)
  ...
end

form_for :user # => syntax error, unexpected tIDENTIFIER, expecting tAMPER or '&' 

In this case, Ruby 1.9.1 won't complain at all, in order to Ruby 1.8 accept this signature you must add an ampersand in the last argument:

def form_for(record, *args, &proc)
  ...
end

form_for @user

form_for @user { |form| ... }

In the last line we are passing the "record" and the "&proc" arguments, not the "args" one. So, inside the method, "args" is an empty array. Let's pass the three arguments.


form_for @user, :url => user_path(@user) { |form| ... }

form_for @user, :as => :person, :url => user_path(@user) { |form| ... }

record  # => @user
*args   # => :as => :person, :url => user_path(@user)
&proc   # => { |form| ... }

2. Passing one array or hash as argument to a method expecting more than one arguments


Passing an array

def foo(bar, baz)
     "bar is:" + bar + " and baz is: " + baz
end

my_ary = ["first", "second"]

foo *my_ary 			 # => bar is: first and baz is: second

foo my_ary[0], my_ary[1] # => bar is: first and baz is: second

my_array = ["first", "second", "third"]

foo *my_array # => wrong number of arguments (3 for 2)

Passing a hash


def foo(bar, baz)
[bar,baz]
end

my_hash = { :first => 1, :second => 2 }

foo *my_hash 	# => [[:first, 1], [:second, 2]]

Consider the following case in Ruby 1.8


def foo(bar, baz, qux)
   [bar, baz, qux]
end

my_ary = ["first", "second"]

foo *my_ary, "third" # => syntax error, unexpected tINTEGER, expecting tAMPER

now in Ruby 1.9.1


foo *my_ary, "third" # => ["first", "second", "third"]

3. Multiple variable assignment


In Ruby, you don't need the splat operator to do multiple variable assignment:


my_ary = [1,2,3]

a, b, c = my_ary

a # => 1
b # => 2
c # => 3

but you need it to do things like the following:


my_ary = [1,2,3]

a, b = my_ary

a #	=> 1
b # => 2

#we lose the third element!

a, *b = my_ary

a # => 1
b # => [2, 3]

Very nice!

A lot of methods in Ruby and Rails returns an array so you can take advantage of that fact and use the splat operator in a bunch of magic ways.

Well, now we know that the splat operator does, and that we can use it in methods, but it can be used in other scenarios as well:

4. Case/When


The following code is an extract of the ActiveRecord::Base#find method in Rails 3:


def find(*args)
   ...
   case args.first
   when :first, :last, :all
      send(args.first)
   else
      find_with_ids(*args)
   end
   ...
end

we can use the splat operator in the when clause this way:


def find(*args)

    VALID_OPTIONS = :first, :last, :all # => [:first, :last, :all]

   case args.first
   when *VALID_OPTIONS
      send(args.first)
   else
     find_with_ids(*args)
   end
   ...
end

User.find(:last) # => send(:last) # => User.last

User.find(:all)  # => send(:all) # => User.all

User.find(1)    # => find_with_ids(1)

User.find(1,4,5,6)   # => find_with_ids(1,4,5,6)

5. Array to Hash / Hash to Array


It sometimes comes in handy converting arrays into hashes and vice-versa, you might do it iterating the elements but there's a kind of shortcut to do it.

Array to Hash

my_ary = [[:first, 1], [:second, 2]]

my_ary.flatten! # => [:first, 1, :second, 2]

my_hash = Hash[*my_ary]

or even shorter


my_hash = Hash[*my_ary.flatten] # => {:first => 1, :second => 2}

my_hash[:first] # => 1
Hash to Array

my_hash = {:first => 1, :second = > 2}

my_ary = *my_hash 	  # => [[:first, 1], [:second, 2]]

Let's see a real world example. The following code was taken from the Thin web server's spec_helper.rb


def parse_response(response)
 raw_headers, body = response.split("\r\n\r\n", 2)
 raw_status, raw_headers = raw_headers.split("\r\n", 2)

 status  = raw_status.match(%r{\AHTTP/1.1\s+(\d+)\b}).captures.first.to_i
 headers = Hash[ *raw_headers.split("\r\n").map { |h| h.split(/:\s+/, 2) }.flatten ] <= HERE!

 [ status, headers, body ]
end

Let's suppose we get a valid HTTP response:


foo_raw_response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 16\r\nConnection: close\r\nServer: thin 1.2.5 codename This Is Not A Web Server\r\n\r\nThis is the BODY"

parse_response(foo_raw_response)

This is the line I'd like you to focus in:


headers = Hash[ *raw_headers.split("\r\n").map { |h| h.split(/:\s+/, 2) }.flatten ]

"raw_headers" is a string we split by \r\n and then by the colon and we ended with an array.


raw_headers = "Content-Type: text/html\r\nContent-Length: 16\r\nConnection: close\r\nServer: thin 1.2.5 codename This Is Not A Web Server"

raw_headers # =>
[
    [0] "Content-Type",   <==== hash key
    [1] "text/html",      <==== hash value
    [2] "Content-Length",
    [3] "16",
    [4] "Connection",
    [5] "close",
    [6] "Server",
    [7] "thin 1.2.5 codename This Is Not A Web Server"
]

The parse_response method will return an array of size 3:


status, headers, body = parse_response(foo_raw_response)

status # => 200
body   # => "This is the body"

headers # =>
{
      "Content-Type" => "text/html",
    "Content-Length" => "16",
        "Connection" => "close",
            "Server" => "thin 1.2.5 codename This Is Not A Web Server"
}

headers[:Content-Type] # => "text/html"

Cool, isn't it? well, we're almost done, let's see another important topic.

6. Object#to_a and Array#to_ary


The Object#to_a method returns an array representation of the receiver.


"string".to_a # => ["string"]
1.to_a # => [1]
(1..5).to_a # => [1, 2, 3, 4, 5]
[1,2,3].to_a # => [1,2,3]

The Array#to_ary method returns self, but of course it only works in arrays.


"string".to_ary # => undefined method
1.to_ary # => undefined method
(1..5).to_ary # => undefined method
[1,2,3].to_ary # => [1,2,3]

So we may say that if an object respond to to_ary we may be sure that such object is an array:


[1,2,3].respond_to? :to_ary # => true (it's an array)

"string".respond_to? :to_ary # => false (it isn't an array)

But what would happen if we'd add a to_ary method to our own class?

Instances of our class would respond to to_ary but they wouldn't be arrays, but what a hell, it'd respond to it, isn't it? so Ruby would treat the instances of our class as if they were arrays because Ruby would be able to get an array representation of them.

Doesn't all this sounds like duck typing?

Let's summarize.

to_a is used to explicitly convert any object to an array.
to_ary is used to implicitly convert any object to an array or to treat objects as arrays.

oh, yeah, well, but what does to_a and to_ary has to do with the splat operator?

The splat operator is often called the "unary operator", because of "unar[ra]y" (unarray), so, at a first glance it'd seem like to_ary and the unary operator are exactly opposite things. Wrong!

In Ruby 1.8 the splat operator will try to invoke to_ary and then try to invoke to_a in the objects you use it. So, if you want your object to be used with the splat operator you have to implement to_ary on it.

In Ruby 1.9 doesn't work like that because the to_a method have been removed. I've read some posts saying there's a to_splat method but it seems it's gone now, I'm using 1.9.2 and it isn't there.If anyone know anything about to_splat, I'd appreciate if you'd let me know.

Let's continue. In order for to_ary to work with the splat operator, it *must* return an array, what if we try to cheat Ruby?


class Baz
  def to_ary
    "this is an array, I swear"
  end
end

my_baz = Baz.new

my_baz.to_ary # => "this is an array, I swear"

If we explicitly send to_ary to "my_baz", Ruby won't complain about it. The problem comes when we do it implicitly.


def bar_method(arg1, arg2, arg3)
   [arg1, arg2, arg3]
end

bar_method(*Baz.new) # => Baz#to_ary should return Array (TypeError)

ok, we couldn't cheat Ruby, so let's fix it.

class Baz
  attr_accessor :first, :second, :third

  def to_ary
    [@first, @second, @third]
  end

end

my_baz = Baz.new

my_baz.first  = 1
my_baz.second = 2
my_baz.third  = 3

bar_method(*my_baz) # => [1,2,3]

Without the to_ary method the result would have been:


bar_method(*my_baz) # => wrong number of arguments (1 for 3)

Last but not least, the to_ary method isn't used only by the splat operator, look at the following code:


first, second, third = my_baz

first # => 1
second # => 2
third # => 3

Thanks for reading, any comment would be appreciated!

About raf

Ruby Developer
Filed under: ruby Comments Off
Comments (21) Trackbacks (2)
  1. Obviously your code allows to compare against more sets in different “when” clauses, yours would be more concise in that case. But for the proposed example (am I improving Rails3?), I still find my solution simpler. Do you know anything about the performance gain? Someone should test that and update Rails accordingly.

    • Check this gist out > https://gist.github.com/783558

      It’s a set of benchmarks to see which is better, run it and please share your results if you can, I think the difference is insignificant, what do you think?

      • I see that your code is more precise than mine…
        Anyway, I put some salt and pepper, check my fork of your gist (https://gist.github.com/783662) and see the real difference.
        The results that I get are consistent with my previous research: the find_with_ids (where the else alternative is followed) shows a huge advantage for the include option (around 40%). But funny enough, the other three alternatives (which is, when the include gets in play) are slower (25-30%). I don’t know how I forgot to check that. Now I should be embarrassed.

  2. In your case/when example, compare yours:

    case args.first
    when *VALID_OPTIONS
    send(args.first)
    else
    find_with_ids(*args)
    end

    with the following:

    if VALID_OPTIONS.include? args.first
    send(args.first)
    else
    find_with_ids(*args)
    end

    which I find much more readable. But anyway, it is very interesting to find new ways of doing things.

    • yes, I think your code is better in certain cases, but the purpose of the post was to show how to use the splat operator, I don’t know if DDH thought that the “when/case” way was better because you can simply add more “whens” instead of modify the “else” and turn it into a “elsif” and add another “else”, I’d think that way. Thanks for reading.

  3. I’ve been working through a number of examples I’ve found on the web. Yours are the “icing” on my “cake”: well designed example in a familiar context (Rails). Thanks for publishing them.

  4. Just wanted to let you know the sidebar looks off in my browser with 1600×1200 resolution.

  5. Keep posting stuff like this i really like it

  6. This is such a great resource that you are providing and you give it away for free. I enjoy seeing websites that understand the value of providing a prime resource for free. I truly loved reading your post. Thanks!

  7. As usual, great post! What a coincidence, I was looking for a way to pass a variable number of arguments to a method. You da man!

  8. What if I don’t want to type or read ‘*’? Is there another code name for this operator?

    • as far as I know no, there isn’t any other way to obtain the same behavior, but I’d like to know if there would be one. Thanks.

  9. When you’re converting from an array to a hash, you should be careful when you start with an array of arrays:

    my_ary = [ [:even, [2,4,6,8]], [:odd, [1,3,5,7,9]] ]
    my_hash = Hash[*my_ary.flatten]
    p my_hash
    # Result:
    # test_splat.rb:3:in `[]‘: odd number of arguments for Hash (ArgumentError)

    For 1.9.1, you can pass an argument to flatten with how many levels to recursively flatten:

    my_ary = [ [:even, [2,4,6,8]], [:odd, [1,3,5,7,9]] ]
    my_hash = Hash[*my_ary.flatten(1)]
    p my_hash
    # Result:
    # {:odd=>[1, 3, 5, 7, 9], :even=>[2, 4, 6, 8]}

  10. Excellent walk through! Thanks

  11. Excellent in deep analysis. Thanks.