Ruby
Ruby - SLT02
Ruby z rychlíku
Ruby a OOP
Ruby Class Hierarchy
DbTalk
Ruby je mladý, (zatím) interpretovaný, objektově orientovaný programovací jazyk, vytvořený a nezávisle vyvíjený Yukihirem Matsumotem. Interpret jazyka je dobře přenositelný a momentálně existuje pro různé varianty UNIXu, MS Windows, Mac OS, OS/2 a BeOS. Nejstručnější charakteristika praví, že Ruby=(Perl+Smalltalk)/2.
Základním zdrojem informací o Ruby je www.ruby-lang.org a vynikající on-line kniha Programming Ruby.
Kdy vás může Ruby zaujmout? Zejména pokud splňujete některou z následujících podmínek:
irb 0.6.1(99/09/16) irb(main):001:0> 1+1 2 irb(main):002:0>Dialog s irb ukončíte stiskem Ctrl+D.
Kód programu vytvořený libovolným editorem lze samozřejmě také uložit do souboru (obyčejně s příponou rb) a spustit příkazem ruby <soubor>.
Obligátní HelloWorld.rb by mohl vypadat například takto:
#!/usr/local/bin/ruby # Nadefinujeme funkci bez parametrů. Definice končí příkazem end. def helloWorld_1 # Funkce puts vypisuje parametry na standardní výstup. puts 'Hello, world!' end # Zavoláme nadefinovanou funkci. helloWorld_1Asi nás nepřekvapí výsledek:
Hello, world!Podívejme se, co lze zjistit pohledem na kód tohoto jednoduchého příkladu:
#!/usr/local/bin/ruby
# Podle toho, zda objekt zadaný parametrem reaguje
# na metodu 'times' funkce zobrazí n krát text nebo
# přímo zadaný parametr.
def helloWorld_2(n)
if n.respond_to?(:times)
n.times { |x| puts "#{x}, Hello, world!" }
else
puts n
end
end
helloWorld_2(3)
helloWorld_2('Konec')
Po spuštění získáme:
0, Hello, world! 1, Hello, world! 2, Hello, world! KonecVidíme, že:
a=33
puts '1: abc \n def #{a}'
puts "2: abc \n def #{a}"
puts `date`
Vypíše:
1: abc \n def #{a}
2: abc
def 33
Fri Feb 1 09:58:58 CET 2002
n=8
7.times do # blok pro opakování je uzavřen mezi 'do' a 'end'
puts "#{n.type} #{n}" # metoda 'type' vrací typ proměnné
n*=n
end
Vypíše:
Fixnum 8 Fixnum 64 Fixnum 4096 Fixnum 16777216 Bignum 281474976710656 Bignum 79228162514264337593543950336 Bignum 6277101735386680763835789423207666416102355444464034512896Celá čísla je také možné zapisovat jako binární, oktalová nebo hexadecimální. Dekadické číslo 513 lze zapsat také jako:
puts -23.abs # absolutní hodnota puts 65.chr # znak, jehož ASCII kód je roven číslu puts 3.next # o jedničku vyšší číslo puts 0.zero? # vrací 'true', když je číslo rovno nule puts 10.remainder(3) # zbytek po dělení čísla parametrem puts 15.type # typ proměnnéJak jistě tušíte, výsledkem je:
23 A 4 true 1 Fixnum
Stojí za zmínku, že většina operátorů, o kterých bude řeč později, jsou ve skutečnosti metody s upravenou syntaxí volání: a+b můžeme napsat jako a+(b), což je volání metody '+' objektu 'a' s parametrem 'b'.
Reálná čísla jsou uchovávána jako objekty třídy Float a to ve dvojnásobné přesnosti. Float poskytuje několik základních metod:
puts 3.14.round # zaokrouhlení podle matematických pravidel puts 3.14.ceil # nejbližší větší celé číslo puts 3.14.floor # nejbližší menší celé číslo puts -3.14.round puts -3.14.ceil puts -3.14.floorNepřekvapí nás výsledek:
3 4 3 -3 -3 -4Zbytek aritmetických operací je soustředěn v tzv. modulu Math a jejich vysvětlení přesahuje rozsah tohoto článku.
s1='Neinterpretuje\nse'
s2="Interpretuje\nse 1+1 = #{1+1}"
s3=<<KONEC
Tento řetězec může mít několik řádků a končí
až slovem, které je uvedeno za znaky '<<'.
(Počítá se do něj i případné odsazení řádků.)
KONEC
puts s1
puts s2
puts s3
Výsledkem je:
Neinterpretuje\nse Interpretuje se 1+1 = 2 Tento řetězec může mít několik řádků a končí až slovem, které je uvedeno za znaky '<<'. (Počítá se do něj i případné odsazení řádků.)Protože práce s řetězci je programátorovým denním chlebem, poskytuje String opravdu velké množství metod pro jejich manipulaci. Uveďme si jen několik jednoduchých příkladů:
puts 'HellO'.downcase # převod do malých písmen
puts 'hello'.upcase # převod do velkých písmen
puts 'hello'.length # délka řetězce
puts 'hello'.include?('el') # vrací 'true', pokud řetězec obsahuje parametr
puts 'hello'.index('el') # pozice parametru v řetězci
puts 'hello'.reverse # obrácený řetězec
puts 'hello'.reverse.reverse # návratová hodnota je zase objekt, jehož
# metodu můžeme volat
puts ' hello '.strip # odstranění prázdných znaků ze začátku a konce
puts 'hello'.tr('he','Ha') # zaměňuje znaky (jako příkaz 'tr' v UNIXu)
Získáme:
hello HELLO 5 true 1 olleh hello hello HalloNavíc můžeme počítat se silnými nástroji pro nahrazování a změny částí řetězců a pro práci s regulárním výrazy, o nichž bude ještě řeč.
Možná jste si již všimli konvence, kdy názvy metod vracejících hodnotu boolean končí otazníkem. Podobně exitují metody, jejichž název končí vykřičníkem. Vykřičník upozorňuje na to, že dochází přímo ke změně samotného objektu. (Normální metody vrací hodnotu, ale stav objektu zůstává nezměněn).
a='hello' puts a puts a.reverse # jen vrať hodnotu puts a puts a.reverse! # vrať hodnotu a změň objekt puts aVrací:
hello olleh hello olleh olleh
puts 2<3 # to je pravda puts 3<2 # to není pravda puts 'abcd'.index 'r' # znak v řetězci není - vrací se doslova "nic"S výsledkem:
true false nil
def puts_ary ary # uděláme si funkci na výpis pole puts ary.join ', ' # vypiš prvky spojené čárkami a mezerami do řetězce end a=['p','o','l','e'] # inicializujeme pole čtyř jednoznakových řetězců puts a.length # počet prvků pole (také můžeme použít a.size) puts a[1] # prvek s indexem 1 (indexy začínají od nuly) puts a[-1] # první prvek od konce pole puts a.index 'o' # index prvku, který se rovná 'o' puts_ary a[2..3] # pole tvořené prvky s rozsahem indexů 2..3 a[4]=1 # přiřadíme další prvek, pole se automaticky rozšíří puts_ary a a.delete_at 4 # zase prvek smažeme (prvek s indexem 4) puts_ary a puts_ary a.sort # získáme seřazené prvky pole puts_ary a.reverse # prvky pole v obráceném pořadí a.push 'x' # přidání prvku na konec pole puts_ary a puts a.pop # odebrání prvku z konce pole puts_ary a a.unshift 'x' # přidání prvku na začátek pole puts_ary a puts a.shift # odebrání prvku ze začátku pole puts_ary aZobrazí se:
4 o e 1 l, e p, o, l, e, 1 p, o, l, e e, l, o, p e, l, o, p p, o, l, e, x x p, o, l, e x, p, o, l, e x p, o, l, ePomocí metod push, pop, shift a unshift lze realizovat klasické struktury zásobníku a fronty. Tyto metody mění dané pole. U jiných platí stejné pravidlo s vykřičníkem jako u řetězců (pro metodu sort existuje ekvivalent sort! atd.)
a={'jedna'=>'I', # hash můžeme inicializovat pomocí čárkami
'dve'=>'II', # oddělených přiřazení klíč=>hodnota
'tri'=>'III',
'ctyri'=>'IV', # v tomto případě použijeme pro klíč i hodnoty
'pet'=>'V' # řetězcové konstanty
}
puts a['dve'] # hodnota s klíčem 'dvě'
puts a['deset'] # hodnota s klíčem 'deset' (není tam, vrací se nil)
a['deset']='X' # přiřadíme klíči 'deset' hodnotu 'X'
puts a['deset'] # hodnota s klíčem 'deset'
puts a.index 'V' # klíč hodnoty 'V'
puts a.invert['IV'] # metoda invert vytvoří obrácený hash (hodnota=>klíč)
puts a.length # počet záznamů v hashi
Po spuštění získáme:
II nil X pet ctyri 6Je možné ukládat do hashů pole a naopak, což nabízí značnou flexibilitu pro konstrukci různých datových struktur. Další možnosti práce s poli a hashi uvidíme v sekci o iterátorech.
a,b,c=1,2,3 # do a,b a c postupně přiřadíme 1,2 a 3
puts "#{a+b+c}" # součet a,b a c
puts 2**5 # dvě na pátou
puts 3<=>5 # porovnání 3 a 5
puts 3>4?1:2 # pokud platí 3>4, zobraz 1, jinak 2
puts 4.between?(3,5) # je 4 mezi 3 a 5?
puts 'a'+'b' # spojení řetězců
puts 'a'*3 # opakování řetězce
Získáme:
6 32 -1 2 true ab aaa
puts 'ahoj'=~/ho/
puts 'ahoj'=~/kk/
puts 'ahoj'!~/ho/
puts 'ahoj'!~/kk/
# složitější regulární výraz pro jednoduchou kontrolu, zda řetězec má formát
# datumu d.m.rrrr
puts '2.2.2002'=~/^[0-9][0-9]?\.[0-9][0-9]?\.[0-9]{4}$/
puts '245.2.2002'=~/^[0-9][0-9]?\.[0-9][0-9]?\.[0-9]{4}$/
Po spuštění uvidíme:
1 nil false true 0 nilRegulární výrazy se používají i jako parametry pro některé metody. Na ukázku si doplníme znalosti metod třídy String:
s='Perl Pike Python Perl Pike Python Perl Pike Python' puts s s.sub!(/Perl/,'Ruby') # nahraď první výskyt slova 'Perl' za slovo 'Ruby' s.gsub!(/Python/,'Ruby') # nahraď každý výskyt slova 'Python' za slovo 'Ruby' puts sVypíše:
Perl Pike Python Perl Pike Python Perl Pike Python Ruby Pike Ruby Perl Pike Ruby Perl Pike RubyPoužití regulárních výrazů je daleko více. Navíc jsme se zabývali jen konstantními výrazy, ale regulární výraz je v Ruby objekt jako každý jiný, a je možné ho bez problémů vytvářet za běhu programu.
a=10 if a>15 then puts 'a>15' # tělo podmínky je oddělené slovem 'then' elsif a>10 then puts 'a>10' elsif a>5 # tělo podmínky je na novém řádku puts 'a>5' else puts 'a<=5' endVýsledkem je pochopitelně:
a>5Použití then není povinné, pokud tělo podmínky začíná až na dalším řádku. Obrácenou podmínku lze realizovat pomocí příkazu unless.
a=0 b=[] # prázdné pole while a<5 b.push a # přidáme hodnotu z a na konec pole b a+=1 end puts b.join ', ' # join vrací řetězec s hodnotami pole spojenými parametremProgram vypíše:
0, 1, 2, 3, 4Jak příkaz if, tak while lze využít ještě jedním elegantním způsobem:
a=0 b=[] puts 'O.K.' if a==0 b.push a+=1 while a<5 puts b.join ', 'Výsledkem je:
O.K. 1, 2, 3, 4, 5Zatímco podmínka if se chová podle očekávání, cyklus poskytuje jiné výsledky než v předchozím případě. Je to způsobeno tím, že již před uložením do pole je proměnná a inkrementována.
3.times { print '*' } # s metodou 'times' jsme se již setkali -
puts # provede blok tolikrát, kolik je hodnota čísla
3.times { |n| print n } # a hodnotu iterace může do bloku předávat jako
puts # parametr
3.upto(5) { |n| print n } # spolu s metodami 'upto' a 'step' prakticky
puts # nahrazuje for cyklus, který v ruby není
3.step(10,2) { |n| print n }
puts
a=['a','b','c'] # malé pole znaků
a.each { print '*' } # pole má také iterátory - each iteruje přes
puts # všechny prvky
a.each { |n| puts "#{n}, Hello!" } # do bloku předává aktuální prvek
b=a.select { |x| x>='b'} # v tomto případě vrací metoda 'select' jen prvky,
puts b.join ', ' # pro které volaný blok vrací true
Výsledkem bude:
*** 012 345 3579 *** a, Hello! b, Hello! c, Hello! b, cIterátorů je v Ruby mnohem více a snadno si lze také vytvořit vlastní. Největší využití najdou zřejmě při nahrazování běžných cyklů, a zejména pro práci s prvky kontejnerů - tj. pole, hashe a dalších.
puts 'a' # vypiš 'a' a odřádkuj print 'b ' # vypiš 'b' a neodřádkuj puts 'c' printf "Číslo: %5.2f, řetězec: %s\n", 3.14, 'PI' # \n bude konec řádkuPo spuštění uvidíme:
a b c Číslo: 3.14, řetězec: PIZjednodušeně řečeno se při volání těchto funkcí ve skutečnosti volá metoda stejného jména náležející objektu třídy IO. Konkrétně se v našem případě jednalo o objekt reprezentující standardní výstup programu, který je přístupný přes globální proměnnou $stdout.
$stdout.puts 'Tak tak' # výpis na stdout(Poznámka pro pozorné čtenáře: znak '$' není na začátku názvu proměnné náhodou. V Ruby existuje konvence pro pojmenování proměnných, která říká, že jména globálních proměnných začínají právě znakem '$'. Ostatní proměnné, které používáme v příkladech jsou lokální a jejich jména začínají na malé písmeno.)
while line=gets # buď načte řádek a pokračuje nebo vrací nil a cyklus končí print line # vypiš řádek na stdout (gets ponechala znak konce řádku) endPokud bychom chtěli nejdříve načíst všechny řádky a pak je zpracovávat, mohli bychom to udělat například takto:
a=$stdin.readlines # načti řádky do pole řetězců puts a.size # počet prvků pole - tj. počet načtených řádků
f=File.new("soubor", "r") # metoda new vytvoří objekt třídy File
while line=f.gets # úplně stejně jako u standardního vstupu
print line
end
f.close # zavřeme soubor
Pro účely přístupu k souboru se využívá třídy File, která je velmi
podobná třídě IO - můžeme volat všechny metody pro vstup a výstup,
které jsme dosud použili. Metoda new, která vytváří nový objekt a v
našem případě otevírá soubor má dva parametry. První určuje název souboru a
druhý mód otevření souboru (např. 'r' znamená read - čtení, 'w' znamená write -
zápis).
Ukažme si ještě obecnost některých principů v Ruby:
f=File.new("soubor", "r") # metoda new vytvoří objekt třídy File
f.each_line { |line| # iterátor volá blok a předává mu řádky
print line
}
f.close # zavřeme soubor
Se souborem můžeme pohodlně pracovat pomocí iterátorů. V našem příkladu jsme
zpracovávali soubor opět po řádcích, ale máme k dispozici i iterátor načítající
soubor po bytech.
Proměnné prostředí jsou uloženy v proměnné ENV, ke které lze přistupovat stejnými metodami jako má třída Hash. Nejlépe je vše vidět na příkladu:
puts "Program: #{$0}"
puts "Argumenty z příkazové řádky: #{ARGV.join(', ')}"
puts "Proměnné prostředí:"
ENV.each { |key,value|
puts "#{key} = #{value}"
}
Pokud uvedený kód bude uložen v souboru program.rb a spustíme jej
pomocí příkazu ruby program.rb par1 par2 par3,
obdržíme výpis podobný tomuto (výpis proměnných je krácen):
Program: test.rb Argumenty z příkazové řádky: par1, par2, par3 Proměnné prostředí: PWD = /home/dali/aoi/prg/ruby/tmp PAGER = less USER = dali COLORTERM = Eterm SHELL = /usr/local/bin/bash HOSTTYPE = i386 OSTYPE = freebsd4.4
#!/usr/local/bin/ruby
begin # začátek bloku s výjimkami
while line=gets # čti každý řádek až do konce souboru
raise "Aaaa!" if line=~"^[aA]" # způsob výjimku, když řádek začíná na 'a'
puts line # vypiš řádek
end # konec cyklu (zde končí běžný program)
rescue RuntimeError # odchyť výjimku třídy 'RuntimeError'
puts "Chyba A!" # zobraz hlášku
rescue Interrupt # odchyť výjimku třídy 'Interrupt'
puts "\nKonec = CTRL-D." # zobraz hlášku
retry # zpět na začátek bloku
else # jinak (tj. pokud nedošlo k výjimce)
puts "\nNedošlo k výjimce." # zobraz hlášku
ensure # tento kód se vykoná tak jako tak -
puts "\nKonec!" # ať došlo k výjice či nikoliv
end # konec bloku s výjimkami
Tento program může skončit bud korektně na konci souboru, který na standardním
vstupu vytvoříme stiskem CTRL-D, nebo vznikem výjimky. Jednu výjimku způsobujeme
v kódu přímo příkazem raise (jedná se o výjimku třídy
RuntimeError. Druhá výjimka třídy Interrupt vznikne
stiskem CTRL-C (tedy požadavkem na přerušení provádění programu).
Zatímco RuntimeError pouze zachytíme, zobrazíme hlášku a program se ukončí, po zachycení Interrupt se pomocí příkazu retry vrátí řízení na začátek bloku označený klíčovým slovem begin. Za příkazem else následuje kód, který se vykoná, pokud k žádné výjimce nedošlo. Za příkazem ensure následuje kód, ketrý se vykoná v každém případě, ať již v bloku k výjimce došlo či ne. Příkazy retry, else a ensure samozřejmě nejsou k fungování mechanismu výjimek nutné. Eventuální nezachycená výjimka ukončí činnost programu.
Podle konkrétních podmínek může spuštění proběhnout různě. Při korektním ukončení takto (je vidět i vstup od uživatele):
^D Nedošlo k výjimce. Konec!Na tomto příkladu vidíme zotavení po stisku CTRL-C a pak ukončení na výjimce vyvolané řádkem začínajícím na 'a':
ppp ppp ^C Konec = CTRL-D. aaa Chyba A! Konec!V obou případech dojde k vypsání zprávy 'Konec!'.
Ošetření chyb a nestandardních situací v programech je zřejmě nejpracnější částí programování. Ačkoliv se jimi již nebudeme zabývat, nabízí Ruby ještě další cesty pro jejich zvládnutí.
#!/usr/local/bin/ruby
# Jednoduchý HTTP server naslouchající na portu 8081 vrací aktuální čas.
# načteme potřebný modul standardní knihovny
require 'socket'
# hlavička dokumentu zaslaná klientovi
head="HTTP/1.1 200/OK\r\nContent-type: text/html\r\n\r\n"
# text zaslaný klientovi - znaky xxx později nahradíme aktuálním časem
text=<<"EOS"
<html>
<body>
<h1>Právě je xxx</h1>
</body>
</html>\r\n
EOS
# vytvoříme instanci TCP serveru
server=TCPServer.new('localhost',8081)
begin # budeme zachytávat výjimku
puts "Start serveru..."
while (session=server.accept) # čekáme na spojení
puts "Požadavek: #{session.gets}" # přečteme data a vypíšeme je na stdout
session.print head # vypíšeme hlavičku
send=text.sub(/xxx/,Time.now.to_s) # nahradíme xxx za aktuální čas
session.print send # vypíšeme připravený text
session.close # uzavřeme spojení
end
rescue Interrupt # stisk CTRL-C korektně ukončí server
puts "\nStop serveru..."
end
Výpisy skriptu mohou vypadat třeba takto:
Start serveru... Požadavek: GET / HTTP/1.0 Požadavek: GET / HTTP/1.0 ^C Stop serveru...Kromě standardní knihovny se stále rozšiřuje množství knihoven vytvořených programátory pro všechny představitelné úlohy. Jejich patrně nejúplnější seznam je k dispozici na stránce The Ruby Application Archive.
#!/usr/local/bin/ruby
#
# factor.rb - triviální faktorizace čísla, příklad ke článku Ruby z rychlíku
#
# Funkce factor vrací první nalezený dělitel čísla n, případně číslo n
# samotné, pokud je to prvočíslo.
def factor n
f=2 # dělitele začneme hledat od 2
while f<=math.sqrt(n) # dělitel nebude větší než odmocnina čísla
return f if n.remainder(f)==0 # pokud je zbytek 0, je f dělitel a končíme
f+=1 # jinak inkrementujeme f a opakujeme cyklus
end
return n # nebyl nalezen dělitel, vracíme n
end
# funkce factorize hledá dělitele čísla n a ukládá je do pole, které je
# návratovou hodnotou. pokud je číslo n prvočíslem, vrátí se v poli jen
# jeden prvek - samotné n.
def factorize n
x,a=n,[] # do x uložíme n a vytvoříme prázdné pole a
while x!=(y=factor(x)) # dokud x není prvočíslo (x==factor x, viz fce factor)
a.push(y) # y je dělitelem x, uložíme y do pole faktorů
x/=y # x vydělíme y a pokračujeme faktorizací výsledku
end
a.push(y) # uložíme poslední výsledek fce factor
return a # vrací se pole faktorů
end
# funkce printfactors vypíše na standardní výstup číslo n a jeho faktory.
def printfactors(n,a)
print n.to_s.ljust(10)+': ' # n převedeme na string a doplníme mezerami
if a.size>1 # pokud jsou v poli faktory,
puts a.join(', ') # vypíšeme je oddělené čárkami,
else # jinak
puts 'je prvočíslo' # je n prvočíslo
end
end
help=<<EOS
factor.rb - triviální faktorizace čísla, příklad ke článku Ruby z rychlíku
použití: factor.rb [číslo1 číslo2 ...]
(pokud nejsou zadány parametry, načítá ze standardního vstupu)
EOS
# Zde začíná hlavní část programu.
if ARGV.size>0 # jsou-li parametry z příkazové řádky,
if ARGV[0]=~/-h|--help/ # zkontroluj, zda není žádost o help
puts help # výpis helpu
exit # normální ukončení programu
end
ARGV.each { |n| # jinak projdi parametry po jednom,
n=n.to_i.abs # převeď na integer a absolutní hodnotu
printFactors(n,factorize(n)) if n>0 # a faktorizuj ty větší než nula
}
else # když nejsou argumenty
begin # budeme zachytávat výjimky
while n=gets # načítej řádky ze standardního vstupu,
n=n.to_i.abs # převeď na integer a absolutní hodnotu
raise if n==0 # pokud to bylo např. písmeno, výjimka
printFactors(n,factorize(n)) # jinak faktorizuj
end
rescue Interrupt # stisk CTRL-C
puts "\n* Stiskněte CTRL-D!" # potřebujeme " kvůli \n
retry # opakujeme od začátku
rescue RuntimeError # 'ručně' vyvolaná výjimka
puts '* Neplatný vstup!'
retry # opakujeme od začátku
end
end
Po spuštění bez parametrů může dialog s uživatelem vypadat například takto:
23456789 23456789 : je prvočíslo 123456789 123456789 : 3, 3, 3607, 3803 a * Neplatný vstup! ^C * Stiskněte CTRL-D! as * Neplatný vstup!