ruby enumerables with an emphasis on reduce
Enumerables primarily traverse or sort collections (ranges, arrays, and hashes) via iteration of a block of code; there's a few big ones: each or each.with_index(), map or collect, all?, none?, any?, count, sort_by, select, and reject
The map or collect, select, and reject methods are enumerables that return a new collection (unlike each or each.with_index()); you don't need to shovel the elements in a result array because these enumerables return a new array that's the result of executing its given block once for each element in its receiver.
# Example 1
simpleton = [1, 2, 3]
simpleton.map {|int| int*2} #=> [2,4,6]
simpleton #=> [1, 2, 3]
# Example 2
about_to_be_slightly_less_simpleton = [1, 2, 3]
about_to_be_slightly_less_simpleton.map! {|int| int**2 #=> [1, 4, 9]
about_to_be_slightly_less_simpleton #=> [1, 4, 9]
# Example 3 (select and reject)
array_of_terms = ["The blab of the pave", "tires of carts",
"sluff of boot-soles", "talk of the promenaders",
"The heavy omnibus", 'the driver with his interrogating thumb']
array_of_terms.select {|t| t.length > 20} #=> ["talk of the promenaders", "the driver with his interrogating thumb"]
array_of_terms.reject {|t| t.length > 20} #=> ["The blab of the pave", "tires of carts", "sluff of boot-soles",
# "The heavy omnibus"]
# WELCOME TO THE DANGER ZONE
array_of_terms.select! {|t| t.length > 20} #=> ["talk of the promenaders", "the driver with his interrogating thumb"]
array_of_terms #=> ["talk of the promenaders", "the driver with his interrogating thumb"]
This is in contrast to the each method:
def line(katz_deli)
line_array = [] # want to create a list that displays the current people in the katz_deli line with their respective place in line
if katz_deli.length == 0 # want to consider if there's any exceptions we might want to consider
puts "The line is currently empty."
else
katz_deli.each.with_index(1) do |name, index| # now we can append our line_array with the people in line and their respective place in line
line_array.push("#{index}. #{name}") # the each method accepts a block of code (between 'do' and 'end' that runs that block of code for every element in the list we're accessing; alternatively we could use a while loop and a global counter variable that starts at 0 and continues as long as it is less than katz_deli.length and for each block iteration, append the line_array with: { (count + 1).to_s + " " + katz_deli[count] }.
end
puts "The line is currently: #{line_array.join(" ")}" # this line will return the elements in line_array as a string
end
end
The all?, none?, or any? methods are enumerables that return whether the element in its receiver is truthy or falsey based on a given block of code:
def none_even?(arr)
# arr.all? {|int| int.odd?} is equivalent
arr.none? {|int| int.even?}
end
def any_even?(arr)
arr.any? {|int| int.even?}
end
The reduce or inject enumerable can be invoked in three ways. Whenever you find yourself setting a variable you reference throughout an iteration, consider using reduce to simplify your code.
1. With one binary argument (:+ or :lcm, for example):
[1, 2, 3].reduce(:+) #=> 6
# this is analogous to:
def my_sum(arr)
accumulator = arr.first # store first element as accumulator
arr.each_index do |idx|
next if idx == 0 # skip first element: it's already the accumulator
#By calling next, you can tell the current block iteration to halt exactly where it is, and tell the parent loop to continue on to the next iteration.
accumulator += arr[idx] # increment accumulator by current element
end
accumulator
end
2. With a block (with an accumulator and the current element) and without an argument; Invoking reduce with a block gives greater control over how to reduce the receiver. One isn't limited to binary methods or operations.
# These two invocations of reduce are functionally equivalent:
[1, 2, 3].reduce(:+)
[1, 2, 3].reduce {|acc, el| acc + el} # It returns acc when no elements remain.
# Example 2
def sum_first_and_odds(arr)
arr.reduce do |acc, el|
if el.odd?
acc + el
else
# this else statement is necessary because otherwise the return value of
# the block would be nil if the element is even. Thus the interpreter
# would reassign acc to nil.
acc
end
end
end
# The accumulator is simply a variable available throughout the iteration that's reassigned after each iteration.
# Example 3
# OLD SOLUTION
def longest_word(str)
words = str.split
longest_word = ""
words.each do |word|
if word.length > longest_word.length
longest_word = word
end
end
longest_word
end
# REDUCED EXCELLENCE
def longest_word(str)
str.split.reduce do |longest, word|
if word.length > longest.length
word
else
longest
end
end
end
sum_first_and_odds([1, 2, 4, 5]) #=> 6
3. With a block and with one argument that's the initial accumulator; the block has two parameters: an accumulator and the current element. What about when we want to use a counter or a result array as the accumulator? The first element wouldn't suffice in most cases:
# There are two differences between invoking reduce with an argument and a block versus with only a block:
# 1. The interpreter initially assigns the accumulator to the given argument.
# 2. The interpreter iterates through the entire receiver, i.e., it does not skip the first element.
# OLD SOLUTION 1
def e_words(str)
words = str.split
count = 0
words.each do |word|
count += 1 if word[-1] == "e"
end
count
end
# REDUCED EXCELLENCE 1
def e_words(str)
str.split.reduce(0) do |count, word|
if word[-1] == "e"
count + 1
else
count # return existing count from block so count isn't reassigned to nil
end
end
end
# Using reduce with an initial accumulator reduces defining a counter variable and iterating through a collection to a single method invocation.
# OLD SOLUTION 2
def boolean_to_binary(arr)
binary = ""
arr.each do |boolean|
if boolean
binary += "1"
else
binary += "0"
end
end
binary
end
# REDUCED EXCELLENCE 2
def boolean_to_binary(arr)
arr.reduce("") do |str, boolean|
if boolean
str + "1"
else
str + "0"
end
end
end
# OLD SOLUTION 3
def factors(num)
factors = []
(1..num).each do |i|
if num % i == 0
factors << i
end
end
factors
end
# REDUCED EXCELLENCE 3
def factors(num)
(1..num).reduce([]) do |factors, i|
if num % i == 0
factors << i
else
factors
end
end
end
Glossary
all? - Passes each element in its receiver to a given block; returns true if the block never returns a falsey value (otherwise it returns false).
any? - Passes each element in its receiver to a given block; returns true if the block ever returns a truthy value (otherwise it returns false).
count - With no arguments: returns the number of elements in its receiver; with one argument: returns the number of elements in its receiver equal to its argument; with a block: returns the number of items in its receiver that, when passed to the block, return a truthy value.
each_with_index - Calls the given block with two arguments--the item and the item's index--once for each element in the method's receiver.
map - Returns a new array that's the result of executing its given block once for each element in its receiver.
none? - Passes each element in its receiver to a given block; returns true if the block never returns a truthy value (otherwise it returns false).
reduce - Combines all elements of its receiver by applying a binary operation, specified by a block or a symbol that names a method or operator; synonymous with inject.
reject - Returns a collection containing all the elements in its receiver for which the given block returns a falsey value.
select - Returns a collection containing all the elements in its receiver for which the given block returns a truthy value.
sort_by - Sorts its receiver by the return values of its elements when they are passed to the given block and returns an array in that order.
with_index - A chainable method that allows the block given to map or each_char to receive indices as well as receiver elements.