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
endclass Arraydef mapcollection = []each{|item| collection << yield(item)}collectionendenddef 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_numbersend
class Arraydef selectitems = []each{|item| items << item if yield(item)}itemsendenddef 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 == emailendnilend
class Arraydef detecteach{|item| return item if yield(item)}nilendenddef 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/iendaless_namesend
class Arraydef rejectselect{|item| !yield(item)}endenddef 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 = 0adjustments.each{|adjustment| total += adjustment}totalend
class Arraydef inject(start)accumulator = starteach{|item| accumulator = yield(accumulator, item)}accumulatorendenddef 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}mapend
class Arraydef each_with_object(object)each{|item| yield(object, item)}objectendenddef 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?}trueend
class Arraydef all?each{|item| return false unless yield(item)}trueendenddef 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?}falseend
class Arraydef any?each{|item| return true if yield(item)}endenddef 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}falseend
class Arraydef include?(item)any?{|member| member == item}endenddef 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 peopleend
Pattern
class Arraydef sortmergesortendenddef 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 Arraydef sort_bysort{|a, b| yield(a) <=> yield(b)}endenddef 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)endfull_listend
class Arraydef flatteneach_with_object([]){|list, item| list.concat(item)}endenddef combine_lists(lists)lists.flattenend
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]] << wordendindexend
class Arraydef group_byeach_with_object(Hash.new{[]}){|obj, item|obj[yield(item)] << item}endenddef 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 endthree_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
- 1,010