Improving your Ruby on Rails Applications with Effective Use of Hash Objects

来源:互联网 时间:1970-01-01

by Sam Phippen

Sam Phippen has been writing software for more than seven years. He routinely gives conference talks about software design and testing and currently serves the Ruby community as a member of the RSpec core team. Sam regularly contributes to Open Source Ruby applications and works as a consultant at Fun and Plausible Solutions . He is the presenter of the recently released video course called“Effective Ruby” from Addison-Wesley Professional .

The Hash class is one of the most widely used inRuby. We use it to represent everything from parameters objects, to database rows, and even domain specific data structures. In this post, we’ll explore a number of specific ways that you can improve your use of hash objects to make better rails applications.

Preferring Hash#fetch over Hash#[]

The most common way to get values from a hash is to use the square brackets, or subscript, method. This method directly looks up the value under the passed key in the hash and returns the value stored under the key in the hash. If the key is not found, [] instance method returns nil .

So let’s talk about fetch . On first inspection, the fetch method is similar to the [] method:

(master)$ irb>> cat = {:say => :meow}=> {:say=>:meow}>> cat.fetch(:say)=> :meow>> (master)$irb>>cat={:say=>:meow}=>{:say=>:meow}>>cat.fetch(:say)=>:meow>>

Unlike the [] method, the fetch method raises an exception if the key being looked up is not found.

This behavior of fetch is very useful. Primarily, it means that you can find key places where data values are missing in your system with ease. It may be the case that you sometimes want to provide a default value, even when the hash does not contain a value stored under that key. With [] you may be used to || ing the value in, using the nil as the missing value behavior. fetch makes this easy and explicit.

Instead of having a separate ( || ) syntax for providing a default the fetch method provides us with an explicit way of doing this. Fetch actually has three signatures:

fetch(key) which raises an exception if the provided key is not found fetch(key, default) which returns the provided default value if the provided key is not found fetch(key) { ... } which evalutes the provided block and returns its return value if the key is not found. Using different hash constructors to provide default values

It’s quite common to create a hash literal using the empty curly brackets syntax {} or to provide some values in the hash with the hashrocket {:a => :b} or symbol key {a: :b} syntax. These are useful when you don’t need a default value in your hash, but there are other forms.

Consider the following example:

value = "hello world I am Sam"letter_counts = {}value.each_char do |char| letter_counts[char] ||= 0 letter_counts[char] += 1end value="hello world I am Sam" letter_counts={} value.each_chardo|char| letter_counts[char]||=0 letter_counts[char]+=1end

We’re counting the occurences of each character in the String, but to do so we’re first initialising the value stored under that key in the hash to zero. We’re doing extra work that we don’t need to. The Hash class has two additional constructor forms we can use to make this easier: which constructs an empty hash that will store the provided value to the constructor under any missing key when it is accessed. { |hash, key| ... } which constructs an empty hash that will invoke the block provided for any missing key which is accessed. The return value of the block will then be stored in that missing key.

These additional forms are useful for when we know we have some default value. We can eliminate the ||= memoization trick from our code. This puts the default value and the construction of the hash in the same place, reducing the cognitive overhead, and performance cost, of our collection of values.

Using each_with_object to transform collections

In the previous example I showed counting the number of times individual characters occur in a string, which is a simple transformation of that string collection. We can, however, achieve this transform with fewer lines of code using a standard protocol on Enumerable in Ruby called each_with_object . Consider:

value = "hello world I am Sam"letter_counts = value.each_char.each_with_object( do |char, hash| hash[char] += 1end value="hello world I am Sam" letter_counts=value.each_char.each_with_object(|char,hash| hash[char]+=1end

This code achieves the same thing, but instead of leaking the letter counts local variable before it is complete, we return it from the each_with_object call. We build the hash inside this call, incrementing the values inside the block provided to each_with_object .

These are some simple, but powerful, uses of Hash objects in Ruby. Finding opportunities to use them within your Rails or Ruby applications will almost certainly yield more readable and usable code. The overarching point here is that the Ruby standard library is very powerful. Being aware of how it works, what’s inside it, and what is possible with it will make you a better Ruby developer. Hash is one of the most fundamental collections, and these and other applications of it will change the way you work with the Ruby language.

Learn More Ruby Best Practices in Safari

You can watch my entire video series“Effective Ruby” in Safari — which is over four hours long and is based on the book of the same name by Peter Jones .