Enumerable, Ruby'de koleksiyonlara ortak arama, süzme, dönüştürme ve özetleme metotları kazandıran bir modüldür. Array, Hash, Range, Set ve Enumerator bu modülü içerir. map her elemanı dönüştürür, select koşulu sağlayanları, reject sağlamayanları tutar, reduce (diğer adı inject) bütün koleksiyonu tek bir değere indirger. sum, tally, partition, zip, min_by ve sort_by ise sık yazılan döngülerin hazır hâlleridir.

Bu yazıda bu metotları gerçek çıktılarıyla gösteriyor, hangisinin ne döndürdüğünü bir tabloda özetliyoruz. Dizilere özgü metotlar Array yazısında, hash'e özgü olanlar Hash yazısında anlatılıyor; tembel (lazy) zincirler için de Enumerator ve lazy yazısına bakabilirsiniz. Ruby'ye yeni başlıyorsanız Ruby öğrenme rehberi iyi bir başlangıç noktası.

Enumerable nasıl çalışır?

Enumerable nasıl çalışır?

Bir sınıfın Enumerable metotlarını kullanabilmesi için iki şey yeter: include Enumerable satırı ve elemanları sırayla yield eden bir each metodu. Modüldeki neredeyse bütün metotlar işini bu each üzerinden yapar. Aşağıdaki küçük sınıf yalnızca each tanımlıyor ama map, sort_by, include? gibi metotların hepsi kendiliğinden çalışıyor:

ruby
class Raf
  include Enumerable

  def initialize(*kitaplar)
    @kitaplar = kitaplar
  end

  def each(&blok)
    @kitaplar.each(&blok)
    self
  end
end

raf = Raf.new("Tutunamayanlar", "Kürk Mantolu Madonna", "İnce Memed")
raf.map(&:size) # => [14, 20, 10]
raf.sort_by(&:size).first # => "İnce Memed"
raf.include?("İnce Memed") # => true
raf.to_a.size # => 3

Kendi sınıfınızda Enumerable ve Comparable kullanmayı Comparable ve Enumerable yazısında daha ayrıntılı ele alıyoruz. Blokların ve yield anahtar kelimesinin nasıl çalıştığı ise blok ve yield yazısında.

map, select, reject ve filter_map

map (diğer adı collect) her eleman için bloğu çalıştırır ve sonuçlardan yeni bir dizi kurar. select (diğer adları filter, find_all) bloğun doğru (truthy) döndürdüğü elemanları, reject ise yanlış (nil veya false) döndürdüğü elemanları toplar. filter_map süzme ve dönüştürmeyi tek adımda yapar: bloğun döndürdüğü doğru değerleri tutar.

ruby
sayilar = 1..10
sayilar.map { |n| n * n } # => [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
sayilar.select(&:even?) # => [2, 4, 6, 8, 10]
sayilar.reject(&:even?) # => [1, 3, 5, 7, 9]
sayilar.filter_map { |n| n * 10 if n > 7 } # => [80, 90, 100]
sayilar.map { it + 1 }.first(3) # => [2, 3, 4]

&:even? yazımı, sembolü bir bloğa çevirir ve { |n| n.even? } ile aynı işi yapar. Son satırdaki it, Ruby 3.4 ile gelen örtük blok parametresidir; ayrıntılar it ve numaralı parametreler yazısında.

reduce (inject) ve sum

reduce bir biriktiriciyle (memo) çalışır: blok her adımda biriktiriciyi ve sıradaki elemanı alır, döndürdüğü değer bir sonraki adımın biriktiricisi olur. Başlangıç değeri verirseniz ilk adımda o kullanılır; vermezseniz ilk eleman biriktirici olur ve döngü ikinci elemandan başlar. Blok yerine bir metot adı (sembol) da verebilirsiniz.

ruby
[2, 3, 4].reduce(1) { |carpim, n| carpim * n } # => 24
[2, 3, 4].inject(:*) # => 24
%w[a b c].reduce("") { |metin, harf| metin + harf.upcase } # => "ABC"
(1..100).sum # => 5050
(1..4).sum { |i| i * i } # => 30
%w[ab cd].sum("") # => "abcd"
[].reduce(:+) # => nil
[].sum # => 0

Toplama için reduce(:+) yerine sum kullanın: hem daha okunaklıdır hem de boş koleksiyonda nil yerine 0 döner. Sayı olmayan değerleri toplarken (örneğin string) sum metoduna uygun bir başlangıç değeri vermeniz gerekir; varsayılan başlangıç 0 olduğu için %w[ab cd].sum TypeError verir.

each_with_object ve tally: sayma ve gruplama

each_with_object ve tally: sayma ve gruplama

each_with_object, reduce ile benzer bir iş yapar ama biriktiriciyi bloktan döndürmenize gerek yoktur: verdiğiniz nesne her adıma aynen geçer ve sonunda o nesne döner. Bir hash ya da dizi doldururken bu, "bloğun son satırında biriktiriciyi döndürmeyi unutma" hatasını ortadan kaldırır. Dikkat: parametre sırası |eleman, nesne| şeklindedir, reduce ile terstir.

ruby
kelimeler = %w[elma armut elma kiraz armut elma]
kelimeler.each_with_object(Hash.new(0)) { |k, sayac| sayac[k] += 1 } # => {"elma" => 3, "armut" => 2, "kiraz" => 1}
kelimeler.tally # => {"elma" => 3, "armut" => 2, "kiraz" => 1}
kelimeler.uniq.each_with_object({}) { |k, h| h[k] = k.size } # => {"elma" => 4, "armut" => 5, "kiraz" => 5}

tally her farklı elemanı anahtar, kaç kez görüldüğünü değer yapan bir hash döndürür. Birden fazla koleksiyonu aynı sayaçta toplamak isterseniz tally metoduna var olan bir hash verebilirsiniz.

partition ve group_by: ikiye ya da gruplara ayırma

partition her zaman iki elemanlı bir dizi döndürür: ilki bloğun doğru döndürdüğü elemanlar, ikincisi geri kalanlar. Sonucu çoklu atamayla iki değişkene almak yaygın bir kalıptır. group_by ise bloğun döndürdüğü değere göre istediğiniz kadar gruba ayırır ve bir hash döndürür.

ruby
cift, tek = (1..6).partition(&:even?)
cift # => [2, 4, 6]
tek # => [1, 3, 5]
(1..6).group_by { |n| n % 3 } # => {1 => [1, 4], 2 => [2, 5], 0 => [3, 6]}
%w[ankara bursa adana bolu].group_by { |s| s[0] } # => {"a" => ["ankara", "adana"], "b" => ["bursa", "bolu"]}

zip: koleksiyonları yan yana dizmek

zip her konumdaki elemanları diğer koleksiyonların aynı konumdaki elemanlarıyla eşleştirir. Sonuç, çağırdığınız koleksiyonun uzunluğundadır: diğerleri kısaysa eksik yerler nil olur, uzunsa fazla elemanlar atılır.

ruby
adlar = %w[Ada Bora Cem]
puanlar = [90, 75]
adlar.zip(puanlar) # => [["Ada", 90], ["Bora", 75], ["Cem", nil]]
adlar.zip(puanlar).to_h # => {"Ada" => 90, "Bora" => 75, "Cem" => nil}
[1, 2].zip([3, 4], [5, 6]) # => [[1, 3, 5], [2, 4, 6]]

min_by, max_by, sort_by: bir ölçüte göre seçme ve sıralama

Karşılaştırmayı elemanın kendisine değil, ondan türettiğiniz bir değere göre yapmak istediğinizde _by sonekli metotlar kullanılır. min_by ve max_by sayı argümanı alırsa birden fazla eleman döndürür. sort_by her zaman yeni bir dizi verir; belgelere göre eşit anahtarlı elemanların sırası garanti edilmez.

ruby
urunler = [
  {ad: "kalem", fiyat: 15},
  {ad: "defter", fiyat: 40},
  {ad: "silgi", fiyat: 8}
]
urunler.min_by { |u| u[:fiyat] } # => {ad: "silgi", fiyat: 8}
urunler.max_by { |u| u[:fiyat] }[:ad] # => "defter"
urunler.min_by(2) { |u| u[:fiyat] }.map { |u| u[:ad] } # => ["silgi", "kalem"]
urunler.sort_by { |u| -u[:fiyat] }.map { |u| u[:ad] } # => ["defter", "kalem", "silgi"]
urunler.sum { |u| u[:fiyat] } # => 63
[5, 3, 9].minmax # => [3, 9]

Hash üzerinde bu metotlar her girişi [anahtar, deger] çifti olarak görür; bu yüzden {a: 2, b: 1}.min_by { |_, v| v } sonucu [:b, 1] gibi bir çifttir. Bu ayrımın tamamı Hash yazısında örneklerle anlatılıyor.

Hangi metot ne döndürür?

Hangi metot ne döndürür? map / collect, select / filter, reject, filter_map
MetotDöndürdüğüNot
map / collectyeni diziher eleman için blok sonucu
select / filterdizi (Hash#select hash)blok doğru dönenler
rejectdizi (Hash#reject hash)blok yanlış dönenler
filter_mapdiziblok sonucunun doğru olanları
reduce / injecttek değerboş koleksiyonda başlangıçsız nil
sumtek değerbaşlangıç varsayılanı 0
each_with_objectverilen nesneparametre sırası eleman, nesne
tallyhasheleman => adet
partitioniki diziden dizidoğru olanlar, diğerleri
group_byhashblok sonucu => eleman dizisi
zipdizi dizisieksik yerler nil
min_by / max_byeleman ya da dizin verilirse dizi
sort_byyeni dizieşitlerin sırası garanti değil

Zincirleme ve tembel değerlendirme

Enumerable metotları dizi döndürdüğü için arka arkaya zincirlenebilir. Her adım yeni bir ara dizi üretir; çok büyük ya da sonsuz koleksiyonlarda bunun yerine lazy kullanılır. lazy, metotları yalnızca ihtiyaç duyulan elemanlar için çalıştıran bir Enumerator::Lazy döndürür:

ruby
(1..20).select(&:even?).map { |n| n * n }.first(3) # => [4, 16, 36]
(1..Float::INFINITY).lazy.select(&:even?).map { |n| n * n }.first(3) # => [4, 16, 36]

İlk satırda 20 elemanın hepsi işlenir, ikinci satırda ise sonsuz bir aralıktan yalnızca gereken üç sonuç hesaplanır. Ayrıntılar Enumerator ve lazy yazısında.

Yaygın hatalar

  • reduce bloğunda biriktiriciyi döndürmemek: reduce({}) { |h, x| h[x] = 1 } ikinci adımda h olarak 1 alır. Bloğun son ifadesi biriktirici olmalı ya da each_with_object kullanılmalı.
  • each_with_object ile reduce parametre sırasını karıştırmak: reduce |memo, eleman|, each_with_object |eleman, memo| alır.
  • Yan etki için map kullanmak: yalnızca ekrana yazacaksanız each yeterlidir; map gereksiz bir dizi üretir.
  • Sayı olmayanları başlangıçsız sum ile toplamak: %w[a b].sum hata verir, sum("") yazın.
  • select ile find'ı karıştırmak: select bütün eşleşmeleri dizi olarak, find yalnızca ilk eşleşmeyi döndürür.

Sık Sorulan Sorular

reduce ile inject arasında fark var mı?

Yoktur. Belgelerde reduce, inject metodunun diğer adı olarak geçer. Aynı şekilde map ile collect, select ile filter da aynı metotlardır.

map ile each arasındaki fark nedir?

each bloğu her eleman için çalıştırır ve koleksiyonun kendisini döndürür. map bloğun sonuçlarından yeni bir dizi kurar. Dönüşüm istiyorsanız map, yalnızca bir işlem yapacaksanız each kullanın.

Enumerable metotları Hash'te neden dizi döndürüyor?

Enumerable, hash'in each metodunun verdiği [anahtar, deger] çiftleri üzerinde çalışır ve genel olarak dizi kurar. Hash sınıfı select ve reject gibi bazı metotları kendisi yeniden tanımladığı için onlar hash döndürür; map, filter_map, sort_by gibi diğerleri dizi döndürür. Hash istiyorsanız sonucu to_h ile çevirin.

Ruby 4.0 Enumerable tarafında ne değiştirdi?

Ruby 4.0 NEWS dosyasına göre Set artık çekirdek sınıf ve Enumerable#to_set metoduna argüman vermek kullanımdan kaldırılma yolunda (deprecated). Array tarafında find ve yeni rfind eklendi. Set için Ruby Set yazısına bakın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Ruby öğrenme rehberi · Ruby Array metotları · Ruby Hash metotları · Enumerator ve lazy · Blok ve yield · Comparable ve Enumerable kendi sınıfınızda