Collections in Ruby

Enumerator and Enumerable (1)

By now, you're already familiar with this kind of code:

[5, 3].each { |e| puts e }

But now, let's try execute this first part of the code only:

[5, 3].each

What's the result of the code above?

Enumerator and Enumerable (2)

Enumerable is Ruby's way of saying that we can get elements out of a collection, one at a time. While Enumerator is an objectification of enumeration.

enumerator = [3, 7, 14].each
enumerator.each { |e| puts e + 1 }

Enumerator and Enumerable (3)

Enumerable is a module used as a mixin in the Array class. It provides a number of enumerators like "map", "select", "inject". The Enumerable module itself doesn't define the each method. It's the responsibility of the class that is including this module to do so.

This Array class, consequently, defines the each method. It returns an object of the type Enumerator when no block is given (like our example in the previous slide). It yields a value of the type self when there is one, like in the snippet below:

[4, 8, 15, 16, 23, 42].each { |e| puts e }

Enumerator and Enumerable (4)

What's the point then to return an Enumerator instance? Consider this snippet below:

enum = [0, -1, 3, 2, 1, 3].each_with_index
p enum.select { |element, index| element < index }

By assigning Enumerator instance to variable "enum" we can chain Enumerable's method called "each_with_index" with another method called "select".

Each with Index (1)

Enumerable provides us with a lot of enumerator methods. First, we will learn about "each_with_index".

[4, 8, 15, 16, 23, 42].each_with_index { |e, i| puts "#{e} -- #{i}" }

Considering you may have used "each_with_index" in our exercises before this, you may be thinking that it is used for Hash only. Actually, it's a method of Enumerable module that can be used for any class that includes it. Both Hash and Array class include Enumerable module in its mixin.

Each with Index (2)

In Hash, however, iterating over a Hash will almost always give you the key and value in array. So you can do something like this:

{:locke => "4", :hugo => "8"}.each_with_index do |kv, i| 
  puts "#{kv} -- #{i}"
end

Map (1)

Map is another Enumerable's method. It works a lot like each with one significant difference: map directly transforms the object that it iterates over.

arr1 = [1, 2, 3, 4] 
arr2 = [1, 2, 3, 4]

arr1.each { |e| e + 1 }
puts arr1

arr2.map { |e| e + 1 }
puts arr2

"Map" returns the resultant array, "each" returns the original array.

Map (2)

Map, however, always returns an array. Knowing this fact, try to complete this method:

def hash_keys(hash)
  # your code here
end

hash_keys({"a" => 1, "b" => 2, "c" => 3})
# returns ["a", "b", "c"]

Map (3)

Solution:

def hash_keys(hash)
  hash.map { |key, value| key }
end

Inject (1)

Inject is a powerful enumerator that can do iteration, accumulation and transformation all at once. Here's an example using an Array:

[4, 8, 15, 16, 23, 42].inject(0) do |accumulator, iterated|
  accumulator += iterated
  accumulator
end

# or even:

[4, 8, 15, 16, 23, 42].inject(0) { |accumulator, iterated| accumulator + iterated }

Inject (2)

Consider the first example:

[4, 8, 15, 16, 23, 42].inject(0) do |accumulator, iterated|
  accumulator += iterated
  accumulator
end
  • The 0 argument is an optional default value.
  • This optional default value is assigned to the first argument accumulator in the block. If there is no value, then accumulator starts with the first value of the array. This is done only once on the first iteration.
  • inject is also an iterator, the second argument iterated being the element it's currently on.
  • accumulator here, is basically incrementing itself by adding the value of iterate to itself.

Inject (3)

We can even use "inject" to transform an array into a hash:

[4, 8, 15, 16, 23, 42].inject({}) { |a, e| a.update(e => e) }

All? Any? None? (1)

The methods "all?", "any?", and "none?" are some useful querying enumerators that always return true or false.

[4, 8, 15, 16, 23, "42"].any? { |e| e.class == String }
[4, 8, 15, 16, 23, "42"].all? { |e| e.class == Fixnum }
[4, 8, 15, 16, 23, "42"].none? { |e| e.class == Float }

All? Any? None? (2)

With Hash, you can use these methods with one of these:

# With one argument that is a 2 element array of the key-value pair
# candidate[0] is the key and candidate[1] is its value
{:locke => 4, :hugo => 8}.any? { |candidate| candidate[1] > 4 }

# or

# With two arguments
{:locke => 4, :hugo => 8}.any? { |candidate, number| number < 4 } 

Union (1)

Ruby provides you with operations to do operations between two arrays. Can you guess what's the result of this expression?

union = ["a", "b", "a"] | ["c", "c"]
# what is the value of union?

Union (2)

Here is the result:

union = ["a", "b", "a"] | ["c", "c"]
# the value of union is ["a", "b", "c"]

Union operator always returns unique elements.

Intersection (1)

Can you guess what's the result of this expression?

intersection = ["a", "b", "c", "c"] & ["b", "b", "c", "d"]
# what is the value of intersection?

Intersection (2)

Here is the result:

intersection = ["a", "b", "c", "c"] & ["b", "b", "c", "d"]
# the value of intersection is ["b", "c"]

Intersection operator also always returns unique elements.

Difference (1)

Can you guess what's the result of this expression?

difference = [1, 2, 3, 1, 2, 3] - [1]
# what is the value of difference?

Difference (2)

Here is the result:

difference = [1, 2, 3, 1, 2, 3] - [1]
# the value of difference is [2, 3, 2, 3]

Difference operator preserves the duplicate elements in the original array, however all occurrence of each element in second array are removed from the result - including the duplicates.

Object References (1)

Enumerables are collection of objects. Hence, it is important for us to understand the underlying concept of object reference.

"What is a reference?" you may wonder. Before we answer that, take a look at this snippet and try to guess what's the result:

hero1 = "Batman"
hero2 = "Robin"
superheroes = [hero1, hero2]
hero2 = "Superman"

puts superheroes

Object References (2)

Surprise!

hero1 = "Batman"
hero2 = "Robin"
superheroes = [hero1, hero2]
hero2 = "Superman"

puts superheroes
# the result would be:
# Batman
# Robin

But why?

Object References (3)

Now, let's try this one:

hero2 = "Robin"
puts "#{hero2}, #{hero2.object_id}"
# let's say that this returns "Robin, 70349816793320"

hero2 = "Superman"
puts "#{hero2}, #{hero2.object_id}"
# let's say that this returns "Robin, 70349827955280"

You will see different object_id printed out for the same variable. If you remember our first lesson on object, you will note that when we assign new string "Superman" to "hero2", it will generate a new object (hence it has a new object_id).

Before the assignment, variable "hero2" is a reference to a string object with object_id "70349816793320". After the assignment, "hero2" refers to a new string object with object_id "70349827955280".

Object References (4)

Now, let's try this one:

hero1 = "Batman"
puts hero1.object_id # let's say this returns "70349819621360"

hero2 = "Robin"
puts hero2.object_id # let's say this returns "70349819639400"

superheroes = [hero1, hero2]
# this block below will return "70349819621360" and "70349819639400"
superheroes.each do |hero|
  puts hero.object_id 
end

hero2 = "Robin"
puts hero2.object_id
# this block below will also return "70349819621360" and "70349819639400"
superheroes.each do |hero|
  puts hero.object_id
end

From snippet above, we can conclude that when we insert variables to an array, the array actually stores the reference to objects of those variables. Therefore, when you change what the variables refer to, the elements inside the array still refer to the old objects.

Object References (5)

Here comes the bang!

hero1 = "Batman"
hero2 = "Robin"
puts "#{hero2}, #{hero2.object_id}"
superheroes = [hero1, hero2]
puts superheroes

hero2.sub!("Robin", "Superman")
puts "#{hero2}, #{hero2.object_id}"
puts superheroes

You will see that object_id of "hero2" both before and after "sub!" does not change. That's because "sub!" actually changes the object. That's also why "!" operators are considered dangerous, it mutates objects that invoke it.

Therefore, when you print the array, the value of its second element changes accordingly.

Collections in Ruby

By qblfrb

Collections in Ruby

  • 263