Як працюють масиви в Ruby: практичні приклади each, map, select, inject, reduce, filter_map

Обкладинка допису: Як працюють масиви в Ruby: практичні приклади each, map, select, inject, reduce, filter_map
Масиви - одна з найзручніших структур даних у Ruby. Вони гнучкі, динамічні й мають величезну кількість вбудованих методів, які дозволяють обробляти дані максимально елегантно. Розберімо основні з них на прикладах.

each - простий обхід елементів

Метод each ітерує масив і виконує блок коду для кожного елемента. Зазвичай його використовують, коли потрібно щось зробити з елементами, але не створювати новий масив.
[1, 2, 3, 4, 5].each do |n|
  puts "Number: #{n}"
end
Результат:
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
=> [1, 2, 3, 4, 5]
each завжди повертає оригінальний масив, а не результат виконання блоку. Що це значить? Блок виконує puts (бачите надруковане "Number 1" і так далі). А повертає ітератор масив => [1, 2, 3, 4, 5]
Тобто якщо передати значення ітератора змінній - ми отримаємо масив.
myvar = [1, 2, 3, 4, 5].each do |n|
  puts "Number: #{n}"
end

Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
=> [1, 2, 3, 4, 5]

myvar
=> [1, 2, 3, 4, 5]

map - трансформація елементів

Метод map (або collect) створює новий масив, у якому кожен елемент - це результат виконання блоку.
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n ** 2 }

p squares  # => [1, 4, 9, 16, 25]
Використовуйте map, коли потрібно отримати новий набір даних із наявного масиву.
Чим відрізняється map від collect?
У Ruby map просто є більш звичною назвою для програмістів, які прийшли з інших мов (JavaScript, Python тощо). А collect - це історична назва, яка залишилася з ранніх версій Ruby (під впливом Smalltalk).
Якщо подивитись у код Ruby (Enumerable модуль):
alias collect map
тобто це точний синонім, не обгортка, не делегат - просто інше ім’я того ж методу.

select - фільтрація елементів

Метод select повертає масив елементів, для яких блок повертає true.
numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.select { |n| n.even? }

p even_numbers  # => [2, 4]
Існує зворотний метод - reject, який повертає елементи, для яких блок повертає false.
odd_numbers = numbers.reject { |n| n.even? }
p odd_numbers   # => [1, 3, 5]

inject / reduce - акумуляція значення

inject (синонім reduce) - це потужний інструмент для "згортання" масиву в одне значення. Можна підрахувати суму, побудувати хеш тощо.
numbers = [1, 2, 3, 4, 5]

sum = numbers.inject(0) { |acc, n| acc + n }
p sum  # => 15
Він бере початкове значення (у нашому випадку 0) і передає його разом з кожним елементом масиву у блок.
Синтаксис:
array.inject(initial_value) { |accumulator, element| ... }
  • accumulator - змінна, у якій зберігається поточний результат.
  • element - поточний елемент масиву.
  • Блок повертає нове значення для accumulator.
Ще приклад - об’єднати елементи в рядок (string):
words = ["Ruby", "is", "fun"]
sentence = words.reduce("") { |acc, word| acc + word + " " }
p sentence.strip  # => "Ruby is fun"
strip використали щоб прибрати зайвий пробіл вкінці.
Побудова хешу буде виглядати так:
letters = %w[a b c]
indexed = letters.inject({}) { |acc, l| acc[l] = l.upcase; acc }
# => {"a"=>"A", "b"=>"B", "c"=>"C"}
Але тут трохи складніше. Метод inject (або reduce) проходить по колекції і поступово накопичує результат у акумуляторі (acc).
Тут ми передаємо {} - тобто початкове значення акумулятора - порожній хеш.
{ |acc, l| acc[l] = l.upcase; acc }
Це блок, який викликається для кожного елемента l у масиві.
  • acc - це поточний накопичувач (спочатку {})
  • l - поточна літера ("a", потім "b", потім "c")
Всередині блоку:
acc[l] = l.upcase
додає в хеш пару:
  • ключ: l (наприклад, "a")
  • значення: l.upcase (тобто "A")
Після цього ми повертаємо сам acc, щоб передати його в наступну ітерацію
Звісно це можна зробити й іншим шляхом, але тут ми розглядаємо можливості саме цих методів.

filter_map - Ruby 2.7+ магія

Метод filter_map об’єднує map і select в один прохід масиву. Він перетворює і відфільтровує одночасно.
numbers = [1, 2, 3, 4, 5, 6]
even_squares = numbers.filter_map { |n| n**2 if n.even? }

p even_squares  # => [4, 16, 36]
Це зручніше та ефективніше, ніж numbers.select { ... }.map { ... }.

Цей допис поки що не має жодних доповнень від автора/ки.

Як працює self, protected і private (Ruby)
28 жовт., 13:52

Як працює self, protected і private (Ruby)

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska
Цілочисельне ділення у Ruby: чому 6 / 4 дорівнює 1
28 жовт., 14:10

Цілочисельне ділення у Ruby: чому 6 / 4 дорівнює 1

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska
28 жовт., 14:42

Як працює &:to_s у Ruby і що таке Symbol#to_proc

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska
Що таке Proc і Lambda в Ruby?
28 жовт., 15:57

Що таке Proc і Lambda в Ruby?

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska
Що відбувається, якщо викликати [1, 2, 3].map(&Person)
29 жовт., 17:54

Що відбувається, якщо викликати [1, 2, 3].map(&Person)

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska
Singleton class (eigenclass) у Ruby: що це і навіщо потрібно
29 жовт., 18:29

Singleton class (eigenclass) у Ruby: що це і навіщо потрібно

Нотатки про Ruby та RoR
Нотатки про Ruby та RoR@kovbaska