Ruby po kapkách (6.) – iterátory a Enumerable

20. března 2009

V minulém díle jsme se zabývali dvěma nejběžnějšími složitějšími datovými typy – polem a hashem. Co mají tyto dvě struktury společného? V obecné rovině se jedná o kolekce objektů, ke kterým můžeme přistupovat přímo pomocí indexu. V případě pole je indexem číslo, v případě hashe libovolný objekt, který reaguje na volání hash navrácením unikátního klíče. Procházet obě kolekce můžeme ale i sekvenčně – prvek po prvku. S využitím stávajících znalostí Ruby bychom mohli takto iterovat přes pole pomocí postupného zvyšování indexu ve smyčce. V případě hashe bychom mohli použít stejný postup s tím, že bychom hash nejdříve převedli na pole hodnot pomocí volání values.

Pozor! Následující příklad ukazuje výše popsaný postup, který může být blízký mnoha programátorům odkojeným klasickými procedurálními jazyky. V Ruby se to ale takhle rozhodně nedělá, i když to, jak je vidět, také funguje. (Rozhodně diskutabilní je v tomto případě opakované volání values ve smyčce, což není zrovna efektivní algoritmus.)

irb(main):001:0> a = [‚a‘, ‚b‘, ‚c‘] => [„a“, „b“, „c“] irb(main):002:0> 0.upto(2) { |i| puts a[i] }
a
b
c
=> 0
irb(main):003:0> h = { 1 => ‚jedna‘, 2 => ‚dve‘, 3 => ‚tri‘ }
=> {1=>“jedna“, 2=>“dve“, 3=>“tri“}
irb(main):004:0> 0.upto(2) { |i| puts h.values[i] }
jedna
dve
tri

Pro procházení vestavěných kolekcí nám Ruby nabízí takzvané iterátory. Jsou to vlastně obyčejné metody, které předávají do bloku postupně všechny prvky kolekce. Základním iterátorem je each. V případě hashe předává each do bloku jak klíč tak hodnotu. Proto má třída Hash navíce definovány iterátory each_key pro iteraci jen přes klíče a each_value pro iteraci jen přes hodnoty.

irb(main):005:0> a.each { |e| puts e }
a
b
c
=> [„a“, „b“, „c“] irb(main):006:0> h.each { |k, v| puts „#{k} / #{v}“ }
1 / jedna
2 / dve
3 / tri
=> {1=>“jedna“, 2=>“dve“, 3=>“tri“}
irb(main):007:0> h.each_key { |k| puts k }
1
2
3
=> {1=>“jedna“, 2=>“dve“, 3=>“tri“}
irb(main):008:0> h.each_value { |v| puts v }
jedna
dve
tri
=> {1=>“jedna“, 2=>“dve“, 3=>“tri“}
irb(main):009:0> h.values.each { |v| puts v }
jedna
dve
tri
=> [„jedna“, „dve“, „tri“]

Pro úplnost je v závěru uvedena i varianta s převodem hashe na pole hodnot. Tentokrát ovšem k přechodu dochází pouze jednou a iteruje se nad výsledným polem. Povšimněte si zřetězení volání metod. Návratovou hodnotou values je objekt, který má definovanou metodu each. Takto řetězit lze opakovaně, což přispívá k úspornému zápisu kódu. V návrhu jazyka se na to myslí a návratové hodnoty metod vestavěných tříd jsou přizpůsobeny možnému zřetězení. V příkladech z irb jsou ostatně návratové hodnoty vidět. Umožňuje nám to například toto.

irb(main):010:0> h.each_key { |k| puts k }.each_value { |v| puts v }
1
2
3
jedna
dve
tri
=> {1=>“jedna“, 2=>“dve“, 3=>“tri“}

Pokud se vám práce s kolekcemi pomocí iterátorů líbí, tak tohle je teprve začátek. Každý objekt, který má definovánu metodu each se totiž může snadno stát Enumerable. Zatím nebudeme rozebírat, jak, ani co to přesně znamená. Řekněme si jen, že v okamžiku, kdy je objekt Enumerable, automaticky získá několik velmi zajímavých metod. Ty umožňují s kolekcí provádět:

  • vyhledávání nebo filtrování prvků,
  • třídění,
  • transformace,
  • a agregace.

Předveďme si to postupně na příkladech.

irb(main):001:0> a = [ ‚ryba‘, ‚had‘, ‚pes‘, ‚vrabec‘, ‚osel‘ ] => [„ryba“, „had“, „pes“, „vrabec“, „osel“] irb(main):002:0> a.find { |e| e.size == 3 }
=> „had“
irb(main):003:0> a.find_all { |e| e.size == 3 }
=> [„had“, „pes“] irb(main):004:0> a.include?(‚mezek‘)
=> false
irb(main):005:0> a.reject { |e| e.size == 3 }
=> [„ryba“, „vrabec“, „osel“] irb(main):006:0> a.grep(/r/)
=> [„ryba“, „vrabec“] irb(main):007:0> a.min
=> „had“
irb(main):008:0> a.max
=> „vrabec“
irb(main):009:0> a.sort
=> [„had“, „osel“, „pes“, „ryba“, „vrabec“] irb(main):010:0> a.map { |e| e.capitalize }
=> [„Ryba“, „Had“, „Pes“, „Vrabec“, „Osel“]

Metoda find vyhledá první prvek, pro který předaný blok vrátí true. Rozvláčněji řečeno: Enumerable má definovanou metodu find. Ta postupně pro každý prvek kolekce volá daný blok a prvek mu předává jako parametr. Výraz, který je v bloku uvedený naposledy je zároveň návratovou hodnotou bloku. Protože v bloku máme jen jeden výraz – porovnání délky prvku a čísla 3, je vyhodnocení této podmínky zároveň i návratovou hodnotou bloku. Metoda find vrací první prvek, pro který blok vrátil true.

Návratovou hodnotou metody find_all je pole všech prvků, pro které blok vrátil true. Metoda include? vrací true nebo false v závislosti na tom, jestli je parametr obsažený v kolekci nebo ne. Metoda reject pracuje inverzně k metodě find_all a vrací pole se všemi prvky, pro které blok vrací false. S metodou grep poněkud předbíháme, protože ta jako parametr zpracovává regulární výraz. V příkladu je použitý triviální regulární výraz sestávající z jediného písmena a vyhledává všechny prvky, které obsahují ‚r‘. Metody min a max vracejí minimální a maximální prvek kolekce. V našem případě jsou prvky řetězce a navrácenými hodnotami jsou tedy první a poslední slovo podle abecedy. Metoda sort vrací pole seřazených prvků kolekce.

Velmi užitečná je metoda map, která umožňuje iterovat přes prvky kolekce a zároveň s nimi provádět nějakou operaci. Výstupem je pole návratových hodnot z bloku pro každý prvek. Trošku složitější na pochopení je pak metoda inject, kterou si ukážeme zvlášť. Tato metoda postupně předává do bloku jednotlivé prvky a také návratovou hodnotu předchozího volání bloku (výsledek zpracování předchozího prvku). Klasickým příkladem na vysvětlení fungování metody inject je sečtení prvků pole.

irb(main):001:0> a = [1, 2, 3] => [1, 2, 3] irb(main):002:0> a.inject { |suma, e| suma + e }
=> 6
irb(main):003:0> a.inject(0) { |suma, e| suma + e }
=> 6
irb(main):004:0> a.inject(1) { |suma, e| suma + e }
=> 7

V prvním případě je metoda inject volána bez parametru a postupně předává do bloku tyto dvojice parametrů: 1, 2; 3, 3. Vidíme jednak, že výsledek prvního volání bloku (součet 1 a 2) byl předán jako parametr suma do druhého volání. Také lze vypozorovat, že první volání bloku bylo speciální v tom, že žádný předchozí výsledek ještě nebyl k dispozici. Do parametru suma se tak předal první prvek a samotná iterace začala vlastně až druhým prvek. Pokud by nám toto chování nevyhovovalo, můžeme metodě předat volitelný parametr, který se použije v parametru suma v první iteraci.

Pojďme se teď podívat, jak dlouhý program potřebujeme v Ruby, abychom vytvořili generátor náhodných osmiznakových hesel. Než ho napíšeme, naučíme se ještě pár fíglů. Prvním je třída Range, se kterou jsme se již setkali v indexování řetězců a polí. Zapisuje se literálem sestávajícím z prvního a posledního prvku, které jsou odděleny dvěma případně třemi tečkami. Dvojtečková varianta zahrnuje poslední prvek, trojtečková ne. Tato třída má definovanou metodu each a je Enumerable.

irb(main):001:0> (1..3).each { |n| puts n }
1
2
3
=> 1..3
irb(main):002:0> (‚a’…’c‘).each { |c| puts c }
a
b
=> „a“…“c“

Literál pro konstrukci Range musí být uzávorkován, protože operátor .. má nižší prioritu než tečkový operátor pro volání metody. Objekty, které tvoří hraniční body rozsahu musí být navzájem porovnatelné a musí odpovídat na metodu succ, která vrací následující objekt v pořadí.

Další věc, která se nám bude hodit, že existuje metoda sort_by, která funguje podobně jako sort, ale řadí prvky na základě výsledku předaného bloku. Předposlední věc je metoda rand, která vrací náhodné číslo. A poslední je nepovinný číselný parametr pro metodu first, který umožňuje získat prvních několik prvků pole.

Nyní se můžeme podívat na zmíněný generátor hesel. Pro mírné zjednodušení předpokládejme, že hesla budou složena pouze z malých písmen a až z.

irb(main):001:0> (‚a‘..’z‘).sort_by { rand }.first(8).join
=> „xsepvdwb“
irb(main):002:0> (‚a‘..’z‘).sort_by { rand }.first(8).join
=> „afeunwxh“

Jak je vidět celý program se díky řetězení silných metod podařilo vměstnat do 41 znaků. Jednoduše popsaná funkčnost je následující: rozsah písmen od a do z seřadíme podle náhodného čísla – čili v náhodném pořadí. Pak vezmeme prvních 8 znaků a pomocí metody join z nich uděláme řetězec.

Příště se opět zaměříme na úkoly a vyzkoušíme si více podobných miniprogramů.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *