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  endend
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_numbersend
class Array  def select    items = []    each{|item| items << item if yield(item)}    items  endend
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  nilend
class Array  def detect    each{|item| return item if yield(item)}    nil  endend
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_namesend
class Array  def reject    select{|item| !yield(item)}  endend
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}  totalend
class Array  def inject(start)    accumulator = start    each{|item| accumulator = yield(accumulator, item)}    accumulator  endenddef 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 Array  def each_with_object(object)    each{|item| yield(object, item)}    object  endend
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?}  trueend
class Array  def all?    each{|item| return false unless yield(item)}    true  endend
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?}  falseend
class Array  def any?    each{|item| return true if yield(item)}  endend
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}  falseend
class Array  def include?(item)    any?{|member| member == item}  endend
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  endend
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)}  endend
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_listend
class Array  def flatten    each_with_object([]){|list, item| list.concat(item)}  endend
def 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]] << word  end  indexend
class Array  def group_by    each_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
  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