Keyword Arguments in Ruby 2.0

| Comments

I’m putting together a Ruby 2.0 talk for a local Ruby meetup group and I thought it wouldn’t hurt to write down what I have learned about Keyword Arguments which is a new feature in Ruby 2.0.

Lets start with some pseudo code:

1
2
3
4
5
def link_to(name, url_options = {}, html_options = {})
  puts "name = #{name}"
  puts "url_options = #{url_options}"
  puts "html_options = #{html_options}"
end

Pain points when using multiple hashes for arguments are a) if you just want to specify html_options you need to remember at what position it was and b) in the example above you need to set url_options to either {}, nil or something else just to get to html_options:

1
2
3
4
link_to("Home", {}, :class => "active")
# name = Home
# url_options = {}
# html_options = {:class=>"active"}

Keyword Arguments to the rescue!

1
2
3
4
5
def link_to(name, url_options: {}, html_options: {})
  puts "name = #{name}"
  puts "url_options = #{url_options}"
  puts "html_options = #{html_options}"
end

url_options: {} and html_options: {} looks like Ruby 1.9 Hash syntax. Here’s how you would specify only html_options:

1
2
3
4
link_to("Home", :html_options => { :class => "active" })
# name = Home
# url_options = {}
# html_options = {:class=>"active"}

This may seem like it just makes you type more characters but think about previously mentioned pain points. You no longer need to be concerned about argument position and you don’t need to care about url_options because it gets set to {} by default.

Speaking about defaults, you have to set default value for each keyword argument or you could use **whatever_variable_name at the end (note double **) of method definition which will capture the rest of key/value pairs:

1
2
3
4
5
6
7
8
9
10
def link_to(name, url_options: {}, **other_options)
  puts "name = #{name}"
  puts "url_options = #{url_options}"
  puts "other_options = #{other_options}"
end

link_to("Home", :class => "active", :url_options => { :clickable => false })
# name = Home
# url_options = {:clickable=>false}
# other_options = {:class=>"active"}

Notice how I’m ignoring argument position and specifying url_options last but it still assigns everything correctly. Pretty neat, right?

One more nice thing - you can use another method (of same object) to set default value and you can even reuse previous arguments. Here’s a trivial example just to showcase how it works:

1
2
3
4
5
6
7
8
9
10
11
12
def temperature(celsius, fahrenheit: to_fahrenheit(celsius))
  puts "#{celsius}°C"
  puts "#{fahrenheit}°F"
end
 
def to_fahrenheit(celsius)
  celsius * 9 / 5 + 32
end
 
temperature(36)
# 36°C
# 96°F

So there you go. I like this feature and I’m pretty sure it will be used quite a lot once Ruby 2.0 gets adopted by more developers.

Hash#fetch and Default Value

| Comments

You’ve been using Hash#fetch and are happy with it. You also know that you can specify default return value in case key you are looking for is not present in hash.

1
2
3
4
5
{}.fetch(:test) # => KeyError: key not found: :test

{}.fetch(:test, "test") # => "test"

{}.fetch(:test) { "test" } # => "test"

Line 3 and 5 return the same result so what’s the difference you may ask? Lets use a complex method for a default value which takes a couple of seconds to process. We will simulate slowness with sleep. Here is a code with some benchmarks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require "benchmark"

def expensive_method
  # this will simulate slowness
  sleep 3
  1 + 1
end

Benchmark.bm do |bm|
  bm.report("argument - key not present") do
    {}.fetch(:test, expensive_method)
  end

  bm.report("argument - key present") do
    { :test => "test" }.fetch(:test, expensive_method)
  end

  bm.report("block - key not present") do
    {}.fetch(:test) { expensive_method }
  end

  bm.report("block - key present") do
    { :test => "test" }.fetch(:test) { expensive_method }
  end
end

#                                  user     system      total       real
# argument - key not present    0.000000   0.000000   0.000000 (  3.001072)
# argument - key present        0.000000   0.000000   0.000000 (  3.001089)
# block - key not present       0.000000   0.000000   0.000000 (  3.001100)
# block - key present           0.000000   0.000000   0.000000 (  0.000013)

See how expensive_method gets executed in argument - key present benchmark blocking everything for 3+ seconds even know :test key is present. We want expensive_method to be run only when :test key is not present and that is why you should use block when specifying default value. Block gets executed only when the key is not present as opposed to argument version which always executes expensive_method no matter if the key is present or not. While this probably won’t bite most of you, I still think it is good to know about this “gotcha”.

I learned about this while watching one of the RubyTapas episodes. RubyTapas are short screencasts related to Ruby by Avdi Grimm. If you’re not subscribed to this resource I highly encourage you to do so - knowledge bombs Avdi keeps dropping are really invaluable.

Array#sample

| Comments

There will come time when you’ll have to get a random element from array.

In my early Ruby days I used code like this:

1
2
array = [1,2,3,4,5]
array[rand(array.length)] # => 2

But I think we can all agree that’s not very pretty.

Meet Array#sample. According to the docs it can return one or more random elements from array. Lets see it in action:

1
2
3
4
array = [1,2,3,4,5]
array.sample    # => 3
array.sample(1) # => [5]
array.sample(3) # => [1,3,5]

Pretty cool, right? And I bet some of you didn’t even know about this method.

Now go on and get familiar with other Array methods.

Update: Johan Bruning asked for benchmarks so here are some numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require "benchmark"

array = [1,2,3,4,5,6,7,8,9,10]
n = 500_000

Benchmark.bm do |x|
  x.report("sample") do
    n.times do
      array.sample
    end
  end

  x.report("rand") do
    n.times do
      array[rand(array.length)]
    end
  end

  x.report("shuffle") do
    n.times do
      array.shuffle[0]
    end
  end
end

#             user     system      total        real
# sample    0.070000   0.000000   0.070000 (  0.072051)
# rand      0.160000   0.000000   0.160000 (  0.156800)
# shuffle   0.320000   0.000000   0.320000 (  0.324631)

As you see using rand is twice as slow as sample and using shuffle to randomize array and then return the first element is 4x slower than sample.

Do you know other ways to get a random element? Please share in the comments.