Collection patterns
Where do they come from?
Extracted from repetitive code.
Accepting the part that varies as an argument.
def convert_elements_to_symbols(elements)
symbols = []
elements.each{|el| symbols << el.to_sym}
symbols
end
def transform_to_percentage(ratios)
[].tap do |percentages|
ratios.each{|ratio| percentages << ratio * 100}
end
end
Why should I use them?
- Common vocabulary
- Less code
- Better at declaring intent
- Keeps things at same level of abstraction
Format
- Name, Aliases, and Library
- Problem
- Pattern
The RHYMING Five
(and a close relative)
collect
select
detect
reject
inject
each_with_object
Map
(aka collect)
Problem
I have a collection of items
that I want to convert into
an array of some related set
of items.
Pattern
def convert_elements_to_symbols(elements)
symbols = []
elements.each{|el| symbols << el.to_sym}
symbols
end
class Array
def map
collection = []
each{|item| collection << yield(item)}
collection
end
end
def convert_elements_to_symbols(elements) elements.map(&:to_sym)
end
select
(aka find_all)
Problem
I want to find all the items
in my collection that meet
a particular criteria.
Pattern
def extract_even_numbers(numbers)
even_numbers = []
numbers.each{|number| even_numbers << number if number.even?}
even_numbers
end
class Array
def select
items = []
each{|item| items << item if yield(item)}
items
end
end
def extract_even_numbers(numbers)
numbers.select(&:even?)
end
detect
(aka find)
Problem
I want to find the first item
in my collection that meet
a particular criteria.
Pattern
def find_user_by_email(users, email)
users.each do |user|
return user if user.email == email
end
nil
end
class Array
def detect
each{|item| return item if yield(item)}
nil
end
end
def find_user_by_email(users, email)
users.detect{|user| user.email == email}
end
Bonus
You can also pass a lambda, whose value will be returned when nothing is found.
%w(a b c).detect(->{'default'}) do |i|
i == 'd'
end #=> "default"
Reject
(aka filter, but not in ruby)
Problem
I have a list of items where
I want filter out some items
based on a particular criteria.
Pattern
def names_without_a(names)
aless_names = []
names.each do |name|
aless_names << name unless name =~ /a/i
end
aless_names
end
class Array
def reject
select{|item| !yield(item)}
end
end
def names_without_a(names)
names.reject{|name| name =~ /a/i}
end
Inject
(aka reduce)
Problem
I have a list of items
I want to aggregate
into a single value.
Pattern
def total_adjustment_amount(adjustments)
total = 0
adjustments.each{|adjustment| total += adjustment}
total
end
class Array
def inject(start)
accumulator = start
each{|item| accumulator = yield(accumulator, item)}
accumulator
end
end
def total_adjustment_amount(adjustments)
adjustments.inject(0){|accu, adjustment| accu + adjustment}
end
each_with_object
(aka with_object)
Problem
I have a list of items
that I want to use to
mutate an object.
Pattern
def create_identity_map(users)
map = {}
users.each{|user| map[user.id] = user}
map
end
class Array
def each_with_object(object)
each{|item| yield(object, item)}
object
end
end
def create_identity_map(users)
users.each_with_object({}){|map, user| map[user.id] = user}
end
Inject vs Each_with_object
Use each_with_object when mutating
mutable objects. Use inject when accumulating
immutable values. When possible, prefer
treating values as immutable and use inject.
COllection Predicates
all?
any?
include?
Skipped
none?
one?
All?
Problem
You have a list of
items for which you
want to check if a
condition is true for
all of them.
pattern
def everyone_over_18?(teens)
teens.each{|teen| return false unless teen.over_18?}
true
end
class Array
def all?
each{|item| return false unless yield(item)}
true
end
end
def everyone_over_18?(teens)
teens.all?(&:over_18?)
end
Any?
Problem
You have a list of
items for which you
want to check if a
condition is true for
any of them.
Pattern
def anyone_over_18?(teens)
teens.each{|teen| return true if teen.over_18?}
false
end
class Array
def any?
each{|item| return true if yield(item)}
end
end
def anyone_over_18?(teens)
teens.any?(&:over_18?)
end
Include?
(aka member?)
Problem
You want to check for
the presence of an item
in a list.
Pattern
def valid_name?(name)
VALID_NAMES.each{|valid_name| return true if valid_name == name}
false
end
class Array
def include?(item)
any?{|member| member == item}
end
end
def valid_name?(name)
VALID_NAMES.include?(name)
end
Sorting
sort
sort_by
Skipped
max
max_by
min
min_by
minmax
minmax_by
Sort
Problem
You have an unordered
set of items, that you want
ordered based on some criteria.
Pattern
def order_by_age(people)
return people if people.size <= 1 # already sorted swapped = true while swapped do swapped = false 0.upto(people.size-2) do |i| if people[i].age > people[i+1].age people[i], people[i+1] = people[i+1], people[i] # swap values swapped = true end end end people
end
Pattern
class Array
def sort
mergesort
end
end
def order_by_age(people)
people.sort{|alice, bob| bob.age <=> alice.age}
end
Sort_by
Problem
You have a list of items
you want to order by a
specific attribute
Pattern
class Array
def sort_by
sort{|a, b| yield(a) <=> yield(b)}
end
end
def order_by_age(people)
people.sort_by(&:age)
end
(un)grouping
flatten
group_by
Skipped
partition
chunk
Flatten
Problem
You have a list of lists that you
want in a single list.
Pattern
def combine_lists(lists)
full_list = []
lists.each do |list|
full_list.concat(list)
end
full_list
end
class Array
def flatten
each_with_object([]){|list, item| list.concat(item)}
end
end
def combine_lists(lists)
lists.flatten
end
Group_by
Problem
You want to group items
in a list by an attribute of
those items.
Pattern
def create_index(words)
index = Hash.new{[]}
words.each do |word|
index[word[0]] << word
end
index
end
class Array
def group_by
each_with_object(Hash.new{[]}){|obj, item|
obj[yield(item)] << item}
end
end
def create_index(words)
words.group_by{|word| word[0]}
end
Composition
Demo
Example Start
def words_that_appear_3_times(words) word_counts = Hash.new{0} words.each do |word| word_counts[word] += 1 end
three_peats = [] word_counts.each do |word, count| three_peats << word if count == 3 end three_peats end words = %w(bob bob bob bill bill jim jim jim jim jake jake jake) words_that_appear_3_times(words)
Example end
def words_that_appear_3_times(words)
words.group_by(&:to_s).select{|_, occurrences| occurrences.size == 3}.map{|word, _| word}
end
words = %w(bob bob bob bill bill jim jim jim jim jake jake jake)
words_that_appear_3_times(words)
questions?
Collection manipulation patterns
By blatyo
Collection manipulation patterns
- 847