Ruby Hashes - Intermediate
The hash is a dictionary-like collection that stores unique keys and their values. It is sometimes referred to as a dictionary or associative array in other programming languages. They are similar to arrays in that both are indexed collections of object references. The difference is that arrays are integer indexed while hashes are object indexed. The index can be an object of any type (string, regular expression, and so on) and is usually referred to as the key. The key can then be used to access the value.
Creating a hash
A hash can be easily created using its implicit form:
>> contact = {"first_name" => "Frederick"}
{
"first_name" => "Frederick"
}
Reading the example out loud we have, "the variable contact references a hash with a key of "first_name" which yields, or results in, "Frederick". Notice the key:value pair is enclosed in curly braces { }
. Recall from the previous chapter that arrays are enclosed in square brackets [ ]
. This is an easy way to immediately recognize whether the collection you are working with is an array or hash. The =>
sign can be thought of as 'yields' or 'results in'. In JavaScript ES6 (EcmaScript 6), the =>
symbol has been coined the "arrow function". Specifically, it's commonly referred to as a "fat arrow". In Ruby, the =>
symbol is referred to as a "hash rocket". In earlier versions of Ruby, this was the only option available for mapping a key to a value. Now, we can also use the alternative syntax, which allows us to write more concisely. Taking advantage of symbols we can write code that is less cluttered:
>> contact = { first_name: 'Frederick'}
{
:first_name => "Frederick"
}
# using a symbol as the named key is the preferred method
Hashes can also be created by calling the new constructor.
>> employees = Hash.new
{}
# ruby returns our new hash, which is empty
Accessing Values
Access a value in a hash by passing in the key. This should feel familiar since it's almost the same way we accessed the values in arrays. Working with our contact variable, we can access the value of "first_name" like this:
>> contact[:first_name]
"Frederick"
# we pass in the symbol :first_name because we used a symbol when creating the hash
# if your key is a symbol pass in a symbol, if it is a string pass in a string
Hashes use nil as a default value for keys that do not exist. When we pass in a key that doesn't exist nil should be returned.
>> contact[:last_name]
nil
We can set a different default value by passing it as an argument when calling the new method.
>> employees = Hash.new("Sorry I do not have that employee")
{ }
>> employees["Bob"] = "CEO"
"CEO"
>> employees["Alice"] = "CTO"
"CTO"
>> employees["Reginald"] = "Director of PR"
"Director of PR"
# at this point our 'employees' hash has 3 different employees
>> employees["Freddy"]
"Sorry I do not have that employee"
# when we try to retrieve the value of a non-existent employee we see our custom error message
Did you notice how we were adding elements to our 'employees' hash? It's so intuitive that you may have not realized it's new material. This is because it feels very natural, based on what we already know about Ruby arrays. We specify the hash we are working with, in this case 'employees', then we add a key with an assigned value.
All hashes are derived from the Ruby class Hash and as such inherit certain public instance methods. Hashes respond to the methods .keys
and .values
by printing the corresponding data to the screen. This is useful if you want to retrieve a list of all keys or a all values.
Another method for accessing values in a hash is.fetch
. The .fetch
method returns the value of a one key. If a second argument is passed then it will be used as a default value if no key is found.
>> hash = {a:1, b:2, c:3, d:4, e:5}
{
:a => 1,
:b => 2,
:c => 3,
:d => 4,
:e => 5
}
>> hash.fetch(:a)
1
>> hash.fetch(:f, "no value")
"no value"
Adding elements
Remember, hashes revolve around key:value pairs. The easiest way to add an element to a hash is how we have been doing so already. We specify the hash, 'employees', for example, and then pass in a key:value pair using the syntax [key] = value
. Another method for storing elements in a hash is .store
. The behavior of .store
is very straightforward. It stores a key and corresponding value in the hash upon which it's called.
# pass in the new element as (key, value)
>> employees.store("Freddy", "COO")
"COO"
>> employees["Freddy"]
"COO"
Counting elements
Count the total number of keys in a hash with the .count
, .size
, or .length
methods. Ruby often provides multiple ways to do the same thing. This speaks to the flexibility of the language. The ruby interpreter is more intuitive than other languages, which provides Rubyists with some flexibility in development. In terms of counting items in a collection, there are subtle differences between these methods that you should be aware of. The count method is mixed in from the enumerable module and can be used with a block or an argument to return the number of matches. If no arguments are passed the count method returns the size of the enumerable on which it was invoked. This means that count will traverse the enumerable, possibly degrading the speed of your application. The effect to which your application is affected depends on the size of the enumerable. The size method, on the other hand, simply returns the number of elements. We can conclude that if no arguments are being passed to count, it's likely more beneficial to use the size method. Length is an alias of the size method. They perform the same function so semantic distinction is not important. The takeaway should merely be that there are multiple options for counting the number of items in a collection. Your first choice should be size, or if you're working with a string then it may be more appropriate to use length.
>> hash = {a:1, b:2, c:3, d:4, e:5}
{
:a => 1,
:b => 2,
:c => 3,
:d => 4,
:e => 5
}
>> hash.size
5
Removing elements
There are multiple options for deleting the elements in a hash. To completely empty out the hash use the .clear
method. This will delete all key:values pairs within the hash. Continuing with the hash we created in the previous example:
>> hash
{
:a => 1,
:b => 2,
:c => 3,
:d => 4,
:e => 5
}
>> hash.clear
# clear removes all elements from the hash, returning an empty array
{}
>> hash
{}
To delete a particular key:value pair, use the .delete
method and specify the key you wish to delete.
>> hash = {a:1, b:2, c:3, d:4, e:5, d:6, f:7}
{
:a => 1,
:b => 2,
:c => 3,
:d => 6,
:e => 5,
:f => 7
}
# to delete a specific key:pair we specify the key with the delete method
>> hash.delete(:a)
# ruby returns the value of the key, confirming that '1' was deleted
1
>> hash
{
:b => 2,
:c => 3,
:d => 6,
:e => 5,
:f => 7
}
# notice our hash no longer contains the key:pair 'a:1'
There is also the .delete_if
method, which allows us to specify a block. It is important to note that when iterating over a hash we are iterating over two variables, a key and a value. Using our hash variable from the previous example:
>> hash
{
:b => 2,
:c => 3,
:d => 6,
:e => 5,
:f => 7
}
>> hash.delete_if{|key, value| value ==2 }
# we pass the key, value, specifying
{
:c => 3,
:d => 6,
:e => 5,
:f => 7
}
Miscellaneous
Invert
We can invert the key:pairs with the invert method. As we expect, this swaps the keys and pairs. Creating a hash of the top 3 singles in 1955 we have:
>> people = {"Perez Prado" => "Cherry Pink And Apple Blossom White", "Bill Haley & His Comets" => "Rock Around the Clock", "Mitch Miller" => "The Yellow Rose of Texas"}
{
"Perez Prado" => "Cherry Pink And Apple Blossom White",
"Bill Haley & His Comets" => "Rock Around the Clock",
"Mitch Miller" => "The Yellow Rose of Texas"
}
# the invert method swaps the keys with the values
>> people.invert
{
"Cherry Pink And Apple Blossom White" => "Perez Prado",
"Rock Around the Clock" => "Bill Haley & His Comets",
"The Yellow Rose of Texas" => "Mitch Miller"
}
Merge
As the name suggests, we can merge two hashes together using the .merge
method. The result of .merge
is, of course, a combination of the two hashes. Based on the US census demographic reports, we can create a hash of the most populated cities in the United States in 1950: (Note: populations are given in millions)
>> places = {"New York" => 7.8, "Chicago" => 3.6, "Philadelphia"=> 2.0}
{
"New York" => 7.8,
"Chicago" => 3.6,
"Philadelphia" => 2.0
}
# using our people hash created in the previous example
>> people.merge(places)
{
"Perez Prado" => "Cherry Pink And Apple Blossom White",
"Bill Haley & His Comets" => "Rock Around the Clock",
"Mitch Miller" => "The Yellow Rose of Texas",
"New York" => 7.8,
"Chicago" => 3.6,
"Philadelphia" => 2.0
}
Flatten
A hash can be "flattened" by the .flatten
method. Imagine a hash as a phonebook, a collection of names and numbers paired together. The names and numbers are not arbitrarily combined. Each name has a corresponding number. There exists a bidirectional relationship between them. Flattening the phone book would squish it down until it's completely level. This would destroy the structure of the phonebook leaving only a list of names and numbers with no relationship between them. The result is an array of equivalent data lacking the previous dimensions that established a relationship.
>> people
{
"Perez Prado" => "Cherry Pink And Apple Blossom White",
"Bill Haley & His Comets" => "Rock Around the Clock",
"Mitch Miller" => "The Yellow Rose of Texas"
}
# the people hash has three names with three corresponding songs
>> people.flatten
[
[0] "Perez Prado",
[1] "Cherry Pink And Apple Blossom White",
[2] "Bill Haley & His Comets",
[3] "Rock Around the Clock",
[4] "Mitch Miller",
[5] "The Yellow Rose of Texas"
]
# by flattening the hash we have an array containing the same data but no relationship
Freeze
Collections such as ranges, arrays, hashes, and sets can be frozen. The .freeze
method can be invoked on any collection and the result is an invisible force field that prevents any further changes.
>> hash = {a:1, b:2, c:3, d:4, e:5}
{
:a => 1,
:b => 2,
:c => 3,
:d => 4,
:e => 5
}
>> hash.freeze
{
:a => 1,
:b => 2,
:c => 3,
:d => 4,
:e => 5
}
# by calling the freeze method the hash can no longer be changed
>> hash.clear
RuntimeError: can't modify frozen Hash
# we cannot clear the key:pairs
>> hash[:f] = 6
RuntimeError: can't modify frozen Hash
# we cannot add an item
Since Ruby 1.9, hashes maintain the order in which they are saved. It's incorrect to say that, "arrays are ordered collections and hashes unordered key:value pairs". You can, in fact, rely on the order in which you save a hash as it will remain unchanged.
There are many other notable public instance methods that can be found in the documentation here: Ruby Docs - Hash
Summary
In this chapter, we examined one of Ruby's popular smart collections, the hash. We defined the hash as a collection of key:value pairs encased in curly brackets { }
. One unique feature that surprises most programmers is that Ruby hashes maintain the order in which they are saved.