Hlavní navigace

Ruby v příkladech (2) - Složitější skripty

15. 9. 2005
Doba čtení: 9 minut

Sdílet

Dnes si ukážeme a probereme nějaké skripty, se kterými se můžete setkat v praxi. Před nějakou dobou jsem si chtěl stáhnout vybrané balíčky distribuce Arch Linux. Chtěl jsem mít ale jistotu, že budu mít staženy i všechny závislosti ...

Závislosti mezi balíčky

Podívejme se na poněkud složitější skript. Před nějakou dobou jsem si chtěl stáhnout vybrané balíčky distribuce Arch Linux. Chtěl jsem mít ale jistotu, že budu mít staženy i všechny závislosti. Názvy balíčků mají tvar něco-verze-pkg.tar.gz. Kromě souborů příslušného software obsahuje každý archiv i soubor s názvem .PKGINFO. Je to jednoduchý textový soubor, jehož některé řádky určují závislé balíčky, např.:

…
depend = bash
depend = glibc
… 

Napsal jsem tedy skript, který tyto závislosti kontroluje, a pokud některé nejsou splněny, vypíše je. Protože manipulace se soubory tar není ve standardní knihovně, je potřeba stáhnout soubor archive-tar-minitar-0.5.1.tar.gz, rozbalit jej a nainstalovat pomocí „ruby install.rb“ (obvykle jsou nutná administrátorská práva). Skript vypadá takto (pozor, čísla řádků nejsou součástí skriptu):

 1:  # check dependecies of Arch Linux packages
 2:
 3:  require 'set'
 4:  require 'zlib'
 5:  require 'archive/tar/minitar'
 6:
 7:  deps = SortedSet.new
 8:
 9:  Dir["*.pkg.tar.gz"].each do |file|
10:
11:    tgz = Zlib::GzipReader.new(File.open(file, 'rb'))
12:    rdr = Archive::Tar::Minitar::Reader.new(tgz)
13:    str = String.new
14:    rdr.each do |entry|
15:      next unless entry.name == '.PKGINFO'
16:      str = entry.read(entry.size)
17:      break

18:    end
19:
20:    lines = str.split /\r?\n/
21:    lines.each do |line|
22:      next unless line =~ /^depend =/
23:      dep = line[/^depend = (\w+(?:-[a-z]\w*)*)/, 1]
24:      deps << dep if Dir["#{dep}-*.pkg.tar.gz"].empty?
25:    end

26:
27:  end
28:
29:  puts deps.to_a 

Na řádcích 3 až 5 specifikujeme použití knihovních souborů pro práci s množinami ( set), manipulaci se soubory *.gz ( zlib) a manipulaci s archivy tar ( archive/tar/minitar). Na řádku 9 je vytvořeno pole s názvy souborů s balíčky a začátek bloku iterací přes jednotlivá jména (blok končí na řádku 27).

Řádky 11 až 18 obsahují otevření souboru s balíčkem a extrakci souboru .PKGINFO, jehož obsah je uložen do řetězcové proměnné str.

V dalším „odstavci“ kódu nejprve rozdělíme řetězec z proměnné str na jednotlivé řádky a uložíme je do pole lines (řádek 20). Při iteraci přes jednotlivé řádky (řádek 21) přeskakujeme ty, které nezačínají „ depend =“ (řádek 22), ze zbylých pak vyseparujeme jméno balíčku bez verze a přípon (řádek 23). Pokud se v aktuálním adresáři takový balíček nevyskytuje (tj. není tam soubor, jehož jméno začíná na výše uvedený název, pokračuje pomlčkou a končí na .pkg.tar.gz), tak se jméno balíčku přidá do množiny deps (řádek 24).

Po zpracování všech souborů s balíčky z aktuálního adresáře se množina deps převede na pole a pomocí metody puts vypíše na standardní výstup (řádek 29). Protože deps je objekt třídy SortedSet , vypsané nevyřešené závislosti jsou seřazené podle abecedy. U výše uvedeného skriptu bych chtěl upozornit na jednu „pastičku“: v konstrukci Dir["maska"] , která vrací pole řetězců s názvy souborů vyhovujících masce (viz např. řádek 24), se používá stejná maska jako v příkazovém řádku shellu. Syntaxe je tedy odlišná od regulárního výrazu, který se zadává např. při extrakci podřetězce v konstrukci str[/regexp/] (viz řádek 23).

Podrobnější informace o polích, regulárních výrazech a iteracích si můžete přečíst například v článku Ruby z rychlíku (2). V článku Regulární výrazy v příkladech pak najdete o podrobnosti o regulárních výrazech obecně.

Zadlužte se!

V dnešní době se na nás valí nabídky různých úvěrů – jedna výhodnější než druhá. Vyspělé země prý už doháníme (zatím alespoň v konzumním způsobu života), nicméně stále ještě nejsme dostatečně zadluženi. Pokud náhodou ještě nikomu nic nedlužíte nebo nemáte alespoň nějakou tu kreditní kartu, je nejvyšší čas s tím něco udělat. Člověk bez dluhů je dnes považován za podezřelou existenci a společenského outsidera. Ostatní si o něm nejčastěji myslí, že je na tom finančně tak špatně, že mu banka ani nepůjčí. Ale pozor! Úvěry jsou sice velmi výhodné, většinou však hlavně pro banku. Abyste se v nabídkách mohli lépe orientovat, je dobré spočítat si RPSN různých nabídek. Jenže ouha, výpočet není tak snadný. Vzorec je to sice pěkný, RPSN se však nedá (až na pár speciálních případů) vypočítat přímo, ale musí se hledat pomocí numerických metod. A co na to Ruby? S numerickými výpočty nemá žádný problém.

 1:  # Vypocitej RPSN (rocni procentni sazba nakladu)
 2:
 3:  module RPSN
 4:
 5:    class Platba
 6:      attr_reader :castka, :datum
 7:
 8:      def initialize(castka, datum)
 9:        @castka = castka
10:        case datum
11:        when Date
12:          @datum = datum
13:        when String
14:          @datum = Date.parse(datum)
15:        end

16:      end
17:
18:    end
19:
20:    def find_rpsn(min, max, eps=1.0)
21:      h = (min+max)/2.0
22:      while res = yield(h/100.0) and res.abs > eps
23:        min = h if res < 0
24:        max = h if res > 0
25:        h = (min+max)/2.0
26:      end

27:      h
28:    end
29:
30:    module ArrayExt
31:      def nuluj(rpsn)
32:        prvni = self[0].datum
33:        inject(0) do |sum, platba|
34:          sum + platba.castka/(1+rpsn)**((platba.datum-prvni)/365.0)
35:        end

36:      end
37:    end
38:
39:  end
40:
41:  if $0 == __FILE__
42:
43:    include RPSN
44:
45:    platby = Array.new
46:    platby.extend(ArrayExt)
47:    platby <<

48:      Platba.new(10000, '2004-12-25') <<
49:      Platba.new(2000, '2005-01-05') <<
50:      Platba.new(-3100, '2005-02-15') <<
51:      Platba.new(-3100, '2005-03-15') <<
52:      Platba.new(-3100, '2005-04-15') <<
53:      Platba.new(-3100, '2005-05-15')
54:
55:    puts find_rpsn(0, 100){ |rpsn| platby.nuluj(rpsn)}
56:
57:  end 

Tady poprvé vidíme použití tříd a modulů. Moduly jednak určují jmenný prostor, jednak umožňují některé zajímavé operace. Moduly mohou být do sebe i vnořené (např. modul ArrayExt (ř. 30) je vnořen do modulu  RPSN).

Na řádcích 5 až 16 je definována třída Platba, která reprezentuje jednotlivé platby půjčky (pokud castka > 0) i splátky (castka < 0). Objekty této třídy budou mít read-only atributy castka a datum spojené s proměnnými @castka a @datum (jsou to vnitřní proměnné instance). Metoda initialize se volá při vytváření instance dané třídy – dá se říci, že je to konstruktor. Metoda také ukazuje, jak se řeší „přetěžování“ v dynamických jazycích. Parametrem metody datum může být buď instance třídy Date , nebo řetězec (tj. instance třídy String ) obsahující datum ve formátu "YYYY-MM-DD". Podrobnosti o třídách a objektech najdete také v seriálu Ruby a OOP.

V modulu RPSN se kromě třídy Platba ještě vyskytuje metoda find_rpsn (ř. 20–28). Navzdory specifickému názvu je to jen mírně upravená obecná metoda půlení intervalu. Tato metoda má tři parametry – minimální hodnotu rpsn (obvykle 0), maximální hodnotu rpsn v procentech (v příkladu je použito 100, ale u nevýhodných půjček může být i vyšší). Třetí parametr je přesnost vypočtu, a pokud při volání metody není zadán, je jeho hodnota 1.0 (rozumná hodnota pro běžné půjčky v Kč). To ale není vše, co metoda očekává. Všimněte si klíčového slova yield na řádku 22. Tento příkaz vyvolá blok, který musí následovat za metodou při jejím volání. Do bloku se navíc vloží jeden parametr a výsledek bloku se přiřadí do proměnné res. Podmínkou ukončení numerických iterací je pak, aby absolutní hodnota res byla menší nebo rovna  eps.

Vnořený modul ArrayExt obsahuje metodu nuluj(ř. 31 až 36), o kterou bude později rozšířeno pole obsahující jednotlivé platby. Tato metoda bude počítat podle vzorce RPSN upraveného tak, aby při správné hodnotě rpsn vyšla nula. Na řádku 33 si všimněte metody inject . Tuto metodu lze volat např. na pole (přesněji na instance tříd rozšířených modulem Enumerable ). Je to iterace po prvcích, ale navíc je v bloku použita další proměnná např. pro akumulaci nebo zapamatování hodnoty. Tak lze elegantně zjistit např. součet prvků pole.

Podmíněný příkaz na řádku 41 je konstrukce, která zajišťuje, že se kód mezi řádkem 41 a 57 provede pouze tehdy, pokud se spustí přímo soubor se skriptem. Tento kód se naopak nespustí, pokud je soubor vložen do jiného souboru pomocí require nebo load.

Na řádku 43 importujeme jmenný prostor modulu RPSN (jinak bychom museli pořád psát např. RPSN::find_rpsn). Všimněme si řádků 45 a 46. Nejprve je vytvořena proměnná platby jako prázdné pole (šlo by to zapsat i jako: platby = [] ). Pak je tato instance třídy Array rozšířena o modul ArrayExt definovaný výše. Tím se do objektu platby dostane i metoda nuluj z řádku 31. V jiných jazycích bychom pro stejný výsledek museli vytvořit potomka třídy Array, který obsahuje metodu nuluj. Nicméně proč vytvářet novou třídu kvůli jedné instanci a jedné metodě navíc?

Na řádcích 47 až 53 naplníme pole jednotlivými platbami. Věřitel provede dvě platby (10 a 2 tisíce Kč), dlužník splácí čtyřmi měsíčními splátkami po 3100 Kč. Budete-li chtít skript použít pro vlastní potřebu (bez záruky!), dejte pozor, aby nejstarší platba byla v poli platby jako první (nebo si skript upravte, aby nemusela :-).

Řádek 55 pak spočítá kýženou hodnotu RPSN. U plateb z řádků 47 až 53 by měla RPSN vyjít cca 13.6 %.

Přidávání metod k objektu

Je možné diskutovat, zda výše uvedené přidání metody ke konkrétnímu objektu (tj. instanci) není v tomto případě příliš krkolomné. Možná. Mějte však na paměti, že v Ruby existuje vždy více cest, jak dosáhnout téhož. Pokud chceme získat instanci, která bude obsahovat určitou, námi definovanou metodu, můžeme použít např. jeden z následujících postupů:

  1. Vytvoříme novou třídu (popřípadě jako potomka již existující třídy), která bude obsahovat naši metodu. Pak vytvoříme objekt (instanci) této třídy voláním metody new. Je to téměř stejné jako v Javě, C++ a podobných jazycích. Ve výše uvedeném příkladu bychom mohli vytvořit třídu Array2 jako rozšíření standardní třídy Array

    :

    class Array2 < Array
      def nuluj
        ...
      end
    end
    
    platby = Array2.new
  2. Doplníme tuto metodu do již existující třídy:
    class Array
      def nuluj
        ...
      end
    end
    
    platby = Array.new

    Takto lze rozšířit i standardní třídy, jako je např. výše uvedená třída Array . O tuto metodu budou ale rozšířeny všechny instance třídy Array – včetně již existujících.

  3. Novou metodu lze definovat v modulu, a ten lze vložit do nové nebo již existující třídy. Lze vkládat i více modulů, takže lze tímto přístupem nahradit vícenásobnou dědičnost. Protože nelze vytvořit instanci modulu, tento přístup se trochu podobá interfacům v Javě:
    module ArrayExt
      def nuluj
        ...
      end
    end
    
    class Array2 < Array
      include ArrayExt
    end
    
    platby = Array2.new

    nebo pokud rozšiřujeme již existující třídu:

    module ArrayExt
      def nuluj
        ...
      end
    
    end
    
    class Array
      include ArrayExt
    end
    
    platby = Array.new
  4. Konkrétní objekt lze také rozšířit o metodu (metody) obsaženou v modulu pomocí metody extend , jak jsme to udělali ve výše uvedeném příkladu výpočtu RPSN.
  5. V Ruby existují tzv. singleton methods (neplést s návrhovým vzorem Singleton – Jedináček). Jsou to metody přidávané ke konkrétnímu objektu přímo (resp. pomocí tzv. anonymní třídy):
    platby = Array.new
    
    def platby.nuluj
      ...
    end

    nebo totéž s jinou syntaxí:

    platby = Array.new
    
    class << platby
      def nuluj
        ...
      end
    
    end

Každý způsob se hodí v jiné situaci. Ruby má ale i další možnosti. Objekt může např. „simulovat“, že má nějaké metody, přestože je ve skutečnosti nemá (objekt si může „odchytit“ volání neznámé metody pomocí metody method_missing ). To lze s výhodou využít např. pro různé generátory nebo transformátory XML.

Na závěr bych chtěl zdůraznit základní rozdíl mezi Ruby a jazyky typu C++, Java nebo C# při definování tříd a modulů. Zatímco v C++, Javě a C# se definice tříd zpracovávají při kompilaci programu, v Ruby je definice třídy v podstatě výkonný kód a třída vzniká až v době běhu. Například definice třídy Tisk s metodou  tiskni:

class Tisk
  def tiskni(a)
    puts a
  end
end 

je jen jiná (hezčí) syntaxe pro tento výkonný kód:

UX DAy - tip 2

Tisk = Class.new do
  define_method(:tiskni, proc{ |a| puts a } )
end 

Prostřední řádek by šel ale zapsat i takto s blokem vně

  define_method(:tiskni) { |a| puts a } ) 

Ale dosti teorie, pusťme se raději do trochu složitější aplikace. Její první část si naprogramujeme příště. Mezitím se můžete podívat třeba na „OneLiners“ – kratičké, často jednořádkové prográmky v Ruby.

Byl pro vás článek přínosný?