Ruby/Wersja do druku

Z Wikibooks, biblioteki wolnych podręczników.

Przejdź do: nawigacji, wyszukiwania


Ruby


[edytuj] Spis treści

[edytuj] Podstawy

  1. Czym jest Ruby?
  2. Zaczynamy
  3. Proste przykłady
  4. Łańcuchy znakowe
  5. Wyrażenia regularne
  6. Tablice
  7. Powrót do prostych przykładów
  8. Struktury sterujące
  9. Domknięcia i obiekty procedurowe
  10. Iteratory

[edytuj] Programowanie zorientowane obiektowo

  1. Myślenie zorientowane obiektowo
  2. Metody
  3. Klasy
  4. Dziedziczenie
  5. Przedefiniowywanie metod
  6. Kontrola dostępu
  7. Symbole
  8. Metody sigletonowe
  9. Moduły
  10. Zmienne
  11. Zmienne globalne
  12. Zmienne klasowe
  13. Zmienne instancji
  14. Zmienne lokalne
  15. Stałe klasowe
  16. Przetwarzanie wyjątków: rescue
  17. Przetwarzanie wyjątków: ensure
  18. Akcesory
  19. Inicjalizacja obiektów
  20. Komentarze i organizacja kodu

[edytuj] Licencja

  1. Licencja

[edytuj] Podstawy

[edytuj] Czym jest Ruby?

Ruby jest "interpretowanym językiem skryptowym do szybkiego i prostego programowania zorientowanego obiektowo" -- co to znaczy?

  • interpretowany język skryptowy:
    • możliwość bezpośrednich wywołań systemowych
    • potężne operacje na łańcuchach znakowych i wyrażeniach regularnych
    • natychmiastowa informacja zwrotna podczas rozwoju oprogramowania
  • szybki i prosty:
    • deklaracje zmiennych są niepotrzebne
    • zmienne nie są typizowane
    • składnia jest prosta i konsekwentna
    • zarządzanie pamięcią jest automatyczne
  • programowanie zorientowane obiektowo:
    • wszystko jest obiektem
    • klasy, metody, dziedziczenie, itd.
    • metody singletonowe
    • domieszkowanie dzięki modułom
    • iteratory i domknięcia
  • a ponadto:
    • liczby całkowite dowolnej precyzji
    • wygodne przetwarzanie wyjątków
    • dynamiczne ładowanie
    • wsparcie dla wielowątkowości

Jeśli jakieś pojęcia wydają ci się obce, czytaj dalej i nie martw się. Mantra języka Ruby to szybko i prosto.


[edytuj] Zaczynamy

Najpierw pewnie chcesz sprawdzić, czy masz zainstalowanego Rubiego. W konsoli (znak zachęty oznaczony jest tutaj jako %, oczywiście nie wpisujemy go), wpisz:

% ruby -v

(-v powoduje wypisanie na ekranie wersji Rubiego), następnie naciśnij Enter. Jeśli Ruby jest zainstalowany powinieneś zobaczyć podobną do tej wiadomość:

% ruby -v
ruby 1.8.3 (2005-09-21) [i586-linux]

lub

ruby 1.8.6 (2007-03-13 patchlevel 0) [i386-mswin32]

Jeśli Ruby nie jest zainstalowany, możesz poprosić administratora o jego zainstalowanie, lub zrobić to samemu, ponieważ Ruby jest darmowym oprogramowaniem bez ograniczeń co do instalacji czy użytkowania. Wszystkie ważne informacje dotyczące instalacji, również w języku polskim, znajdziesz na stronie głównej Rubiego.

Zacznijmy zabawę z Rubim. Możesz umieścić program w Rubim bezpośrednio w linii poleceń używając opcji -e:

% ruby -e 'puts "witaj swiecie"'
witaj swiecie

Konwencjonalnie, program w Rubim można umieścić w pliku.

% echo "puts 'witaj swiecie'" > hello.rb
% ruby hello.rb
witaj swiecie

Jeśli masz zamiar pisać bardziej rozbudowany kod, będziesz musiał użyć prawdziwego edytora tekstu!

Niektóre zadziwiająco złożone i przydatne rzeczy mogą zostać zrobione przy użyciu miniaturowych programów, które mogą zmieścić się w jednej linijce linii komend. Np., taki, który zamienia foo na bar we wszystkich plikach źródłowych i nagłówkowych języka C w bieżącym katalogu, dodając do plików oryginalnych rozszerzenie ".bak".

% ruby -i.bak -pe 'sub "foo", "bar"' *.[ch]

Ten program działa jak polecenie cat w UNIX'ie (ale wolniej niż cat):

% ruby -pe 0 file


[edytuj] Proste przykłady

Napiszmy funkcję obliczającą silnię. Matematyczna definicja silni n to:

n! = 1                (gdy n==0)
   = n * (n-1)!       (w innym przypadku)

W Rubim możemy ją napisać w następujący sposób[1]:

def silnia(n)
  if n == 0
    1
  else
    n * silnia(n-1)
  end
end

Warto zauważyć powtarzające się wyrażenie end. Ruby nazywany jest przez to "algolopodobnym" językiem programowania. Właściwie, składnia Rubiego bardziej przypomina inny język - Eiffel. Można także zauważyć brak wyrażeń return. Nie są one potrzebne, ponieważ funkcja w Rubim zwraca ostatnią wartość, która była w niej liczona. Używanie wyrażenia return jest dozwolone, lecz niepotrzebne.

Wypróbujmy naszą funkcję silni. Dodanie jednej linii kodu daje nam działający program:

# Program, który liczy wartość silni z danej liczby
# Zapisz go jako silnia.rb
 
def silnia(n)
  if n == 0
    1
  else
    n * silnia(n-1)
  end
end
 
puts silnia(ARGV[0].to_i)

ARGV jest tutaj tablicą zawierającą argumenty linii poleceń, a to_i konwertuje łańcuch na liczbę całkowitą.

% ruby silnia.rb 1
1
% ruby silnia.rb 5
120

Czy nasz program zadziała z argumentem 40? Nie policzymy tego na kalkulatorze...

% ruby silnia.rb 40
815915283247897734345611269596115894272000000000

Działa! Ruby może sobie poradzić z każdą liczbą całkowitą, która zmieści się w pamięci naszego komputera. Więc możemy policzyć nawet 400!:

% ruby silnia.rb 400
64034522846623895262347970319503005850702583026002959458684
44594280239716918683143627847864746326467629435057503585681
08482981628835174352289619886468029979373416541508381624264
61942352307046244325015114448670890662773914918117331955996
44070954967134529047702032243491121079759328079510154537266
72516278778900093497637657103263503315339653498683868313393
52024373788157786791506311858702618270169819740062983025308
59129834616227230455833952075961150530223608681043329725519
48526744322324386699484224042325998055516106359423769613992
31917134063858996537970147827206606320217379472010321356624
61380907794230459736069956759583609615871512991382228657857
95493616176544804532220078258184008484364155912294542753848
03558374518022675900061399560145595206127211192918105032491
00800000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000

Nie możemy natychmiastowo sprawdzić poprawności obliczeń, ale muszą być prawidłowe. :-)

[edytuj] Pętla wejście/obliczenie i sposób prezentacji przykładów

Kiedy wywołasz Rubiego bez argumentów, interpreter wczyta komendy ze standardowego wejścia i wykona je po zakończeniu wprowadzania:

% ruby
puts "witaj swiecie"
puts "zegnaj swiecie"
^D
witaj swiecie
zegnaj swiecie

Znak ^D powyżej oznacza Ctrl+D, wygodny sposób sygnalizowania, że wprowadzanie zostało zakończone w systemach uniksowych. W DOS/Windows spróbuj użyć F6 lub ^Z.

Ruby zawiera również program zwany irb, który pomaga wprowadzać kod bezpośrednio z klawiatury w interaktywnej pętli, pokazując na bieżąco rezultaty.

Oto krótka sesja z irb:

% irb
irb(main):001:0> puts "Witaj, swiecie."
Witaj, swiecie.
=> nil
irb(main):002:0> exit

"Witaj swiecie" jest wypisane przez puts. Następna linia, w tym przypadku nil, pokazuje cokolwiek, co zostało obliczone jako ostatnie. Ruby nie rozróżnia instrukcji i wyrażeń, więc obliczanie kawałka kodu jest równoważne z jego wykonaniem. Tutaj, nil oznacza, że puts nie zwraca żadnej (znaczącej) wartości. Zauważ, że możemy opuścić pętlę interpretera przez wpisanie exit.

W naszym podręczniku będziemy korzystać z programu irb oraz z przykładów zapisanych bezpośrednio jako kod źródłowy. Rezultat działania takiego kodu (wyjście), będziemy podawać jako komentarz, stosując oznaczenie: #=>. (Co to jest komentarz możesz sprawdzić tutaj.) Alternatywnie, będziemy czasem przedstawiać sesję z naszym kodem jako zapis okna terminala.


[edytuj] Łańcuchy znakowe

Ruby radzi sobie z łańcuchami znakowymi tak dobrze jak z danymi numerycznymi. Łańcuch może być ograniczony znakami podwójnego cudzysłowu ("...") lub pojedynczego (apostrof) ('...').

irb(main):001:0> "abc"
=> "abc"
irb(main):002:0> 'abc'
=> "abc"

Używanie podwójnych lub pojedynczych cudzysłowów czasami może mieć różne efekty. Łańcuch ujęty w podwójny cudzysłów pozwala stosować znaki formatujące za pomocą odwróconego ukośnika oraz obliczać zagnieżdżone wyrażenia używając #{}. Łańcuch ujęty w apostrofy nie pozwala na taką interpretację; to co widzisz - to dostajesz. Przykłady:

irb(main):001:0> puts "a\nb\nc"
a
b
c
=> nil
irb(main):002:0> puts 'a\nb\nc'
a\nb\nc
=> nil
irb(main):003:0> "\n"
=> "\n"
irb(main):004:0> '\n'
=> "\\n"
irb(main):005:0> "\001"
=> "\001"
irb(main):006:0> '\001'
=> "\\001"
irb(main):007:0> "abcd #{5*3} efg"
=> "abcd 15 efg"
irb(main):008:0> var = " abc "
=> " abc "
irb(main):009:0> "1234#{var}5678"
=> "1234 abc 5678"

Manipulowanie łańcuchami w Rubim jest sprytniejsze i bardziej intuicyjne niż w C. Dla przykładu, możesz łączyć ze sobą łańcuch używając +, a powtarzać łańcuch wiele razy za pomocą *:

irb(main):001:0> "foo" + "bar"
=> "foobar"
irb(main):002:0> "foo" * 2
=> "foofoo"

Konkatenacja łańcuchów w C jest bardziej kłopotliwa z powodu konieczności bezpośredniego zarządzania pamięcią:

char *s = malloc(strlen(s1)+strlen(s2)+1);
strcpy(s, s1);
strcat(s, s2);
/* ... */
free(s);

W Rubim natomiast, nie musimy w ogóle zastanawiać się nad miejscem zajmowanym w pamięci przez łańcuch. Jesteśmy wolni od jakiegokolwiek zarządzania pamięcią.

Oto kilka rzeczy, które możesz zrobić z łańcuchami.

Konkatenacja:

slowo = "fo" + "o" 
puts slowo #=> "foo"

Powtórzenie:

slowo = slowo * 2 
puts slowo #=> "foofoo"

Ekstrahowanie znaków (zauważ, że znaki w Rubim są liczbami całkowitymi):

puts slowo[0]     #=> 102
# 102 jest kodem ASCII znaku `f'
 
puts slowo[-1]    #=> 111
# 111 jest kodem ASCII znaku `o'

(Wartości ujemne oznaczają liczbę znaków od końca łańcucha.)

Ekstrahowanie podłańcuchów:

warzywo = "pietruszka"
puts warzywo[0,1]    #=> "p"
puts warzywo[-2,2]   #=> "ka"
puts warzywo[0..3]   #=> "piet"
puts warzywo[-5..-2] #=> "uszk"

Sprawdzanie równości:

puts "foo" == "foo" #=> true
puts "foo" == "bar" #=> false

Zróbmy użytek z kilku tych cech. Oto zgadywanka "co to za słowo", ale być może słowo "zgadywanka" to zbyt dużo dla tego kodu. ;-)

# zapisz to jako zgadnij.rb
slowa = ['fiolek', 'roza', 'bez']
sekret = slowa[rand(3)]
 
print "zgadniesz? "
while odp = STDIN.gets
  odp.chop!
  if odp == sekret
    puts "Wygrales!"
    break
  else
    puts "Przykro mi, przegrales."
  end
  print "zgadniesz? "
end
puts "Chodzilo o ", sekret, "."

Na razie nie przejmuj się za bardzo szczegółami powyższego kodu. Oto jak wygląda uruchomiona łamigłówka.

% ruby zgadnij.rb
zgadniesz? fiolek
Przykro mi, przegrales.
zgadniesz? bez
Przykro mi, przegrales.
zgadniesz? ^D
Chodzilo o roza.

(Mogło nam pójść nieco lepiej biorąc pod uwagę, że szansa na trafienie wynosi 1/3.)


[edytuj] Wyrażenia regularne

Stwórzmy bardziej interesujący program. Tym razem sprawdzimy czy łańcuch pasuje do opisu zakodowanego jako ścisły wzorzec.

Oto pewne znaki i kombinacje znaków które mają specjalne znaczenie w tych wzorcach:

Kombinacja Opis
[] specyfikacja zakresu (np., [a-z] oznacza litery od a do z)
\w litera lub cyfra; to samo co [0-9A-Za-z]
\W ani litera ani cyfra
\s biały znak; to samo co [ \t\n\r\f]
\S nie biały znak
\d znak cyfra; to samo co [0-9]
\D znak nie będący cyfrą
\b backspace (0x08) (tylko jeśli występuje w specyfikacji zakresu)
\b granica słowa (jeśli nie występuje w specyfikacji zakresu)
\B granica nie słowa
* treść stojąca przed tym symbolem może powtórzyć się zero lub więcej razy
+ treść stojąca przed tym symbolem musi powtórzyć się jeden lub więcej razy
{m,n} treść stojąca przed tym symbolem musi powtórzyć się od m do n razy
? treść stojąca przed tym symbolem może wystąpić najwyżej jeden raz; to samo co {0,1}
| albo treść stojąca przed tym symbolem albo następne wyrażenie muszą pasować
() grupowanie
^ początek wiersza
$ koniec wiersza

Wspólny termin określający wzory, które używają tych dziwnych symboli, to wyrażenia regularne. W Rubim tak samo jak w Perlu bierze się je raczej w ukośniki (/) niż w cudzysłowy. Jeżeli nigdy wcześniej nie pracowałeś z wyrażeniami regularnymi, prawdopodobnie nie wyglądają one zbyt regularnie, ale naprawdę warto poświęcić trochę czasu by się z nimi zaznajomić. Wyrażenia regularne są skuteczne i ekspresywne. Oszczędzi ci to bólów głowy (i wielu linii kodu) niezależnie od tego, czy potrzebujesz dopasowywania wzorców, wyszukiwania czy też innego manipulowania łańcuchami.

Dla przykładu, przypuśćmy, że chcemy sprawdzić czy łańcuch pasuje do tego opisu: "Zaczyna się małą literą f, po której zaraz następuje jedna duża litera i opcjonalnie jakieś inne znaki, aż do wystąpienia innych innych małych liter." Jeżeli jesteś doświadczonym programistą C, prawdopodobnie już napisałeś około tuzina linii kodu w głowie, prawda? Przyznaj, prawie nie możesz sobie poradzić. Ale w Rubim potrzebujesz jedynie by łańcuch został sprawdzony pod kątem występowania wyrażenia regularnego /^f[A-Z][^a-z]*$/.

A co z "zawiera liczbę heksadecymalną w ostrych nawiasach"? Żaden problem.

def ma_hex?(s)                      # "zawiera hex w ostrych nawiasach"
  (s =~ /<0(x|X)(\d|[a-f]|[A-F])+>/) != nil
end
 
puts ma_hex?("Ten nie ma.") #=> false
 
puts ma_hex?("Może ten? {0x35}") #=> false 
# (zły rodzaj nawiasów)
 
puts ma_hex?("Albo ten? <0x38z7e>") #=> false
# fałszywa liczba hex
 
puts ma_hex?("Dobra, ten: <0xfc0004>.") #=> true

Chociaż wyrażenia regularne mogą się wydawać na początku nieco zagadkowe, z pewnością szybko osiągniesz satysfakcję z tak ekonomicznego sposobu wyrażania skomplikowanych pomysłów.

Oto mały program który pomoże ci eksperymentować z wyrażeniami regularnymi. Zapisz go jako regx.rb i uruchom przez wpisanie ruby regx.rb w linii poleceń.

# Wymaga terminala ANSI!
 
st = "\033[7m"
en = "\033[m"
 
puts "Aby zakonczyc wprowadz pusty tekst."
 
while true
  print "tekst> "; STDOUT.flush; tekst = gets.chop
  break if tekst.empty?
  print "wzor> "; STDOUT.flush; wzor = gets.chop
  break if wzor.empty?
  wyr = Regexp.new(wzor)
  puts tekst.gsub(wyr,"#{st}\\&#{en}")
end

Program wymaga dwukrotnego wprowadzenia danych. Raz oczekuje na łańcuch tekstowy, a raz na wyrażenie regularne. Łańcuch sprawdzany jest pod kątem występowania wyrażenia regularnego, następnie wypisywany z podświetleniem wszystkich pasujących fragmentów. Nie analizuj teraz szczegółów, analiza tego kodu wkrótce się pojawi.

tekst> foobar
wzor> ^fo+
foobar
~~~

Znaki tyldy oznaczają podświetlony tekst na wyjściu programu.

Wypróbujmy kilka innych tekstów.

tekst> abc012dbcd555
wzor> \d
abc012dbcd555
   ~~~    ~~~

Jeśli cię to zaskoczyło, sprawdź w tabeli na górze tej strony: \d nie ma żadnego związku ze znakiem d, oznacza natomiast pojedynczą cyfrę.

A co, jeśli jest więcej niż jeden sposób poprawnego dopasowania wzoru?

tekst> foozboozer
wzor> f.*z
foozboozer
~~~~~~~~

foozbooz jest spasowany zamiast samego fooz, gdyż wyrażenie regularne wybiera najdłuższy, możliwy podłańcuch.

Oto wzór do wyizolowania pola zawierającego godzinę z separatorem w postaci dwukropka.

tekst> Wed Feb  7 08:58:04 JST 1996
wzor> [0-9]+:[0-9]+(:[0-9]+)?
Wed Feb  7 08:58:04 JST 1996
           ~~~~~~~~

"=~" jest operatorem dopasowania w odniesieniu do wyrażeń regularnych; zwraca pozycję w łańcuchu, gdzie może być znaleziony pasujący podłańcuch lub nil jeżeli takowy nie występuje.

puts "abcdef" =~ /d/ #=> 3
puts "aaaaaa" =~ /d/ #=> nil


[edytuj] Tablice

Możesz stworzyć tablicę przez podanie kilku jej elementów wewnątrz nawiasów kwadratowych ([]) oddzielonych przecinkami. W Rubim tablice mogą przyjmować obiekty różniące się typami.

irb(main):001:0> tab = [1, 2, "3"]
=> [1, 2, "3"]

Tablice mogą być konkatenowane i powtarzane tak jak łańcuchy.

irb(main):002:0> tab + ["pla", "bla"]
=> [1, 2, "3", "pla", "bla"]
irb(main):003:0> tab * 2
=> [1, 2, "3", 1, 2, "3"]

Możemy używać numerów indeksów aby odnieść się do jakiejkolwiek części tablicy.

irb(main):004:0> tab[0]
=> 1
irb(main):005:0> tab[0,2]
=> [1, 2]
irb(main):006:0> tab[0..1]
=> [1, 2]
irb(main):007:0> tab[-2]
=> 2
irb(main):008:0> tab[-2..-1]
=> [2, "3"]
irb(main):009:0> tab[-2,2]
=> [2, "3"]

(Wartości ujemne oznaczają położenie elementu od końca tablicy.)

Tablice mogą być konwertowane na łańcuchy tekstowe i odwrotnie poprzez użycie odpowiednio: join (dołącz) i split (podziel):

irb(main):010:0> tekst = tab.join(":")
=> "1:2:3"
irb(main):011:0> tekst.split(":")
=> ["1", "2", "3"]

Wreszcie, aby dodać dodać nowy element do tablicy (tablice w Rubim zachowują się jak listy) można zastosować operator <<:

irb(main):006:0> tab << 4
=> [1, 2, "3", 4]
irb(main):007:0> tab << "bla"
=> [1, 2, "3", 4, "bla"]

[edytuj] Tablice wielowymiarowe

W języku Ruby można także definiować tablice tablic, przez co można niejako "emulować" ich wielowymiarowość. Spójrzmy na następujący fragment kodu:

irb(main):012:0> t = [[1,2],[3,4]]
=> [[1, 2], [3, 4]]
irb(main):013:0> t[1][0]
=> 3

Jako wynik na ekranie pojawia się cyfra 3 (pierwszy element drugiej tablicy "wewnętrznej").

[edytuj] Tablice asocjacyjne

Tablica asocjacyjna ma elementy, które są dostępne przez klucze mogące mieć wartość dowolnego rodzaju, a nie przez kolejne numery indeksów. Taka tablica jest czasem nazywana hash'em lub słownikiem; w świecie Rubiego preferujemy termin hash. Hash (czyt. hasz) może być utworzony przez pary "klucz => wartość" umieszczone w nawiasach klamrowych ({}). Klucza używa się by odnaleźć coś w haszu, tak jak używa się indeksu by odnaleźć coś w tablicy.

irb(main):014:0> h = {1 => 2, "2" => "4"}
=> {1=>2, "2"=>"4"}
irb(main):015:0> h[1]
=> 2
irb(main):016:0> h["2"]
=> "4"
irb(main):017:0> h[5]
=> nil

Dodawanie nowego wpisu:

irb(main):018:0> h[5] = 10
=> 10
irb(main):019:0> h
=> {5=>10, 1=>2, "2"=>"4"}

Kasowanie wpisu przez podanie klucza:

irb(main):020:0> h.delete 1
=> 2
irb(main):021:0> h[1]
=> nil
irb(main):022:0> h
=> {5=>10, "2"=>"4"}
irb(main):023:0>


[edytuj] Powrót do prostych przykładów

Rozbierzmy na części kod kilku poprzednich przykładowych programów.

Następujący kod pojawił się w rozdziale z prostymi przykładami.

def silnia(n)
  if n == 0
    1
  else
    n * silnia(n-1)
  end
end
puts silnia(ARGV[0].to_i)

Ponieważ jest to pierwsze objaśnienie, zbadamy każdą linię osobno.

[edytuj] Silnie

def silnia(n)

W pierwszej linii, def jest instrukcją służącą do definiowania funkcji (lub, bardziej precyzyjnie, metody. O tym, czym jest metoda będziemy mówić więcej w dalszym rozdziale). Tutaj, def wskazuje, że funkcja przyjmuje pojedynczy argument, nazwany n.

if n == 0

if służy do sprawdzania warunku. Kiedy warunek jest spełniony, następny fragment kodu jest obliczany. W przeciwnym razie obliczane jest cokolwiek co występuje za else.

1

Wartość if wynosi 1 jeżeli warunek jest spełniony.

else

Jeżeli warunek nie jest spełniony, obliczany jest kod znajdujący się od tego miejsca aż do end.

n * silnia(n-1)

Jeżeli warunek nie jest spełniony, wartość wyrażenia if wynosi n razy silnia(n-1).

end

Pierwszy end zamyka instrukcję if.

end

Drugi end zamyka instrukcję def.

puts silnia(ARGV[0].to_i)

Ta linia wywołuje naszą funkcję silnia() używając wartości z linii poleceń oraz wypisuje wynik.

ARGV jest tablicą, która zawiera argumenty z linii poleceń. Elementy ARGV są łańcuchami znakowymi, więc aby dokonać konwersji na liczby całkowite używamy metody to_i. Ruby nie zamienia łańcuchów na liczby automatycznie tak jak Perl.

Co się stanie jeśli podamy naszemu programowi liczbę ujemną? Widzisz problem? Umiesz go rozwiązać?

[edytuj] Łańcuchy znakowe

Teraz zbadamy nasz program - łamigłówkę z rozdziału o łańcuchach znakowych. Ponieważ jest on nieco długi, ponumerujmy linie, by móc się łatwo do nich odwoływać.

  1. slowa = ['fiolek', 'roza', 'bez']
  2. sekret = slowa[rand(3)]
  3.  
  4. print "zgadnij? "
  5. while odp = STDIN.gets
  6.   odp.chop!
  7.   if odp == sekret
  8.     puts "Wygrales!"
  9.     break
  10.   else
  11.     puts "Przykro mi, przegrales."
  12.   end
  13.   print "zgadnij? "
  14. end
  15. puts "Chodzilo o ", sekret, "."

W tym programie użyta jest nowa struktura sterująca - while. Kod pomiędzy while a jej kończącym end będzie wykonywany w pętli tak długo jak pewien określony warunek pozostanie prawdziwy. W tym przypadku odp = STDIN.gets jest zarówno aktywną instrukcją (pobierającą linię wejściową od użytkownika i zachowującą ją jako odp), oraz warunkiem (jeżeli nie ma żadnego wejścia, odp, które reprezentuje wartość całego wyrażenia odp = STDIN.gets, będzie miało wartość nil, która spowoduje przerwanie pętli while).

STDIN oznacza obiekt standardowego wejścia. Zwykle odp = gets robi to samo, co odp = STDIN.gets.

rand(3) w linii 2 zwraca losową liczbę w przedziale od 0 do 2. Ta losowa liczba jest użyta do wyciągnięcia jednego elementu z tablicy slowa.

W linii 5 czytamy jedną linię ze standardowego wejścia przez metodę STDIN.gets. Jeżeli wystąpi EOF (ang. end of file - koniec pliku) podczas pobierania linii, gets zwróci nil. Tak więc kod skojarzony z tą pętlą while będzie powtarzany dopóki nie zobaczy ^D (lub ^Z czy też F6 pod DOS/Windows), co oznacza koniec wprowadzania.

odp.chop! w linii 6 usuwa ostatni znak z odp. W tym wypadku zawsze będzie to znak nowej linii, gdyż gets dodaje ten znak by odzwierciedlić naciśnięcie przez użytkownika klawisza Enter, co w naszym wypadku jest niepotrzebne.

W linii 15 drukujemy tajne słowo (sekret). Zapisaliśmy to jako wyrażenie puts (skrót od ang. put string - dosł. "połóż łańcuch") z dwoma argumentami, które są drukowane jeden po drugim. Można to zapisać równoważnie z jednym argumentem, zapisując sekret jako #{sekret} by było jasne, że jest to zmienna do przetworzenia, nie zaś literalne słowo:

puts "Chodzilo o #{sekret}."

Wielu programistów uważa, że utworzenie pojedynczego łańcucha jako argumentu metody puts to czytelniejszy sposób formułowania wyjścia.

Również my stosujemy puts do standardowego wyjścia naszego skryptu, ale ten skrypt używa również print zamiast puts, w liniach 4 i 13. puts i print nie oznaczają dokładnie tego samego. print wyświetla dokładnie to, co jest podane. puts ponadto zapewnia, że linia na wyjściu posiada znak końca linii. Używanie print w liniach 4 i 13 ustawia kursor dalej, poza uprzednio wydrukowanym tekstem zamiast przenosić go na początek następnego wiersza. Tworzy to rozpoznawalny znak zachęty do wprowadzania danych przez użytkownika.

Poniższe cztery wywołania wyjścia są równoważne:

# nowa linia jest automatycznie dodawana przez puts, jeżeli znak nowej linii jeszcze nie wystąpił:
puts  "Zona Darwina, Esmerelda, zginela w ataku pingwinow."
 
# znak nowej linii musi być jawnie dodany do polecenia print:
print "Zona Darwina, Esmerelda, zginela w ataku pingwinow.\n"
 
# możesz dodawać wyjście stosując +:
print "Zona Darwina, Esmerelda, zginela w ataku pingwinow." + "\n"
 
# lub możesz dodawać podając więcej niż jeden łańcuch:
print "Zona Darwina, Esmerelda, zginela w ataku pingwinow.", "\n"

Jeden słaby punkt: czasami okno tekstowe z powodu prędkości działania posiada buforowane wyjście. Poszczególne znaki są buforowane i wyświetlane dopiero gdy pojawi się znak przejścia do nowej linii. Więc, jeżeli skrypt naszej zgadywanki nie pokazuje zachęty dla użytkownika dopóki użytkownik nie poda odpowiedzi, niemal na pewno winne jest buforowanie. Aby upewnić się, że tak się nie stanie możesz wyświetlić (ang. flush - dosł. "wylać") wyjście jak tylko zostanie wydrukowana zachęta dla użytkownika. flush mówi standardowemu urządzeniu wyjściowemu (obiekt nazwany STDOUT), "nie czekaj - wyświetl to co masz w tej chwili".

  1. print "zgadnij? "; STDOUT.flush
  1. print "zgadnij? "; STDOUT.flush

Będziemy z tym bezpieczniejsi również w następnym skrypcie.

[edytuj] Wyrażenia regularne

W końcu zbadamy program z rozdziału o wyrażeniach regularnych.

  1. st = "\033[7m"
  2. en = "\033[m"
  3.  
  4. puts "Aby zakonczyc wprowadz pusty tekst."
  5.  
  6. while true
  7.   print "tekst> "; STDOUT.flush; tekst=gets.chop
  8.   break if tekst.empty?
  9.   print "wzor> "; STDOUT.flush; wzor=gets.chop
  10.   break if wzor.empty?
  11.   wyr = Regexp.new(wzor)
  12.   puts tekst.gsub(wyr, "#{st}\\&#{en}")
  13. end

W linii 6 w warunku dla pętli while zakodowana jest "na sztywno" wartość true, co w efekcie daje nam nieskończoną pętlę. Aby więc przerwać wykonywanie pętli umieściliśmy instrukcje break w liniach 8 i 10. Te dwie instrukcje są również przykładem modyfikatorów if. Modyfikator if wykonuje wyrażenie po swojej lewej stronie wtedy i tylko wtedy, gdy określony warunek jest prawdziwy. Konstrukcja ta jest niezwykła, gdyż działa logicznie od prawej do lewej strony, ale jest dostępna, ponieważ wielu ludziom przypomina podobne wzorce obecne w mowie potocznej. Dodatkowo jest ona zwięzła - nie potrzebuje wyrażenia end by wskazać interpreterowi ile kodu następującego po if ma być traktowane jako warunek. Modyfikator if jest wygodnym sposobem używanym w sytuacjach, gdzie wyrażenie i warunek są wystarczająco krótkie by zmieścić się razem w jednej linii skryptu.

Rozważmy zmiany w interfejsie użytkownika w stosunku do poprzedniego skryptu - łamigłówki. Bieżący interfejs pozwala użytkownikowi zakończyć program poprzez wciśnięcie klawisza Enter przy pustej linii. Sprawdzamy czy każda linia z wejścia jest pustym łańcuchem, a nie czy w ogóle istnieje.

W liniach 7 i 9 mamy "nie-destruktywny" chop. Pozbywamy się tak niechcianego znaku końca linii, który zawsze otrzymujemy od gets. Jak dodamy wykrzyknik, będziemy mieli "destruktywny" chop. Jaka to różnica? W Rubim istnieje konwencja dołączania znaków ! lub ? do końca nazw pewnych metod. Wykrzyknik (!) oznacza coś potencjalnie destruktywnego, coś co może zmienić wartość przylegającego wyrażenia. chop! zmienia łańcuch bezpośrednio, chop daje ci obciętą kopię bez psucia oryginału. Oto ilustracja tej różnicy.

irb(main):001:0> s1 = "teksty"
=> "teksty"
irb(main):002:0> s1.chop!        # To zmienia s1.
=> "tekst"
irb(main):003:0> s2 = s1.chop    # To tworzy zmienioną kopię pod s2,
=> "teks"
irb(main):004:0> s1              # ... bez wpływu na s1.
=> "tekst"

Czasem będziesz też widział w użyciu chomp i chomp!. Te dwa są bardziej selektywne: końcówka łańcucha jest obcinana tylko wtedy, gdy jest znakiem końca linii. Dla przykładu, "XYZ".chomp! nie zrobi nic. Jeżeli potrzebujesz jakiegoś triku by zapamiętać różnice, pomyśl o osobie lub zwierzęciu, które smakuje coś nim zdecyduje się to ugryźć (ang. chomp - "jeść niechlujnie"), a toporze rąbiącym jak popadnie (ang. chop - "odrąbanie").

Pozostałe konwencje nazywania metod pojawiają się w liniach 8 i 10. Znak zapytania (?) oznacza metodę predykatową (orzekającą o czymś), która zwraca albo prawdę (true) albo fałsz (false).

Linia 11 tworzy obiekt będący wyrażeniem regularnym z łańcucha podanego przez użytkownika. Cała właściwa praca wykonywana jest wreszcie w linii 12, która używa gsub do globalnego podstawienia każdego dopasowania naszego wyrażenia z samym sobą, ale otoczonego przez znaczniki ANSI. Ta sama linia wyświetla również wyniki.

Moglibyśmy podzielić linię 12 na osobne linie, tak jak tutaj:

  1. podswietlony = tekst.gsub(wyr,"#{st}\\&#{en}")
  2. puts podswietlony

lub w "destruktywnym" stylu:

  1. tekst.gsub!(wyr,"#{st}\\&#{en}")
  2. puts tekst

Spójrz ponownie na ostatnią część linii 12. st i en były zdefiniowane w liniach 1-2 jako sekwencje ANSI które odpowiednio zmieniają i przywracają kolor tekstu. W linii 12 są one zawarte w #{} by w ten sposób były właściwie interpretowane (i nie wystąpiły zamiast nich nazwy zmiennych). Pomiędzy nimi widzimy \\&. Jest to trochę podstępne. Ponieważ podstawiany łańcuch zawarty jest w cudzysłowach (podwójnych), para odwróconych ukośników będzie zinterpretowana jako jeden ukośnik, co gsub zobaczy właściwie jako \&. To spowoduje powstanie specjalnego kodu, który z kolei odwoła się do czegokolwiek, co jako pierwsze będzie pasować do wzorca. Tak więc nowy łańcuch, gdy będzie wyświetlony będzie wyglądał tak jak pierwszy z wyjątkiem tego, że te fragmenty które pasują do wzorca będą wyświetlone w odwróconych kolorach.


[edytuj] Struktury sterujące

Ten rozdział odkrywa nieco więcej na temat struktur sterujących Rubiego.

[edytuj] case

Instrukcji case używamy do sprawdzenia sekwencji warunków. Na pierwszy rzut oka jest ona podobna do instrukcji switch z języka C lub Java ale, jak za chwilę zobaczymy, znacznie potężniejsza.

def okresl(i)
  case i
  when 1, 2..5
    puts "1..5"
  when 6..10
    puts "6..10"
  end
end
 
okresl(8) #=> 6..10

2..5 jest wyrażeniem oznaczającym przedział zamknięty od 2 do 5. Następujące wyrażenie sprawdza czy wartość i należy do tego przedziału:

(2..5) === i

case wewnętrznie używa operatora relacji === by sprawdzić kilkanaście warunków za jednym razem. W odniesieniu do obiektowej natury Rubiego, === jest interpretowany odpowiednio dla obiektu który pojawia się jako warunek w instrukcji when. Na przykład, następujący kod sprawdza równość łańcuchów znakowych w pierwszym wyrażeniu when oraz zgodność wyrażeń regularnych w drugim when.

def okresl(s)
  case s
  when 'aaa', 'bbb'
    puts "aaa lub bbb"
  when /def/
    puts "zawiera def"
  end
end
 
okresl("abcdef") #=> zawiera /def/

[edytuj] while

Ruby dostarcza wygodnych sposobów do tworzenia pętli, chociaż jak odkryjesz w następnym rozdziale, wiedza o tym jak używać iteratorów często zaoszczędzi ci bezpośredniego pisania własnych pętli.

while jest powtarzanym if. Używaliśmy tej instrukcji w nasze słownej zgadywance i w programach sprawdzających wyrażenia regularne (zobacz poprzedni rozdział). Tutaj instrukcja ta przyjmuje formę while warunek ... end otaczającą blok kodu który będzie powtarzany dopóty, dopóki warunek jest prawdziwy. Ale while i if mogą być łatwo zaaplikowane również do pojedynczych wyrażeń:

irb(main):001:0> i = 0
=> 0
irb(main):002:0> puts "To jest zero." if i == 0
To jest zero.
=> nil
irb(main):003:0> puts "To jest liczba ujemna" if i < 0
=> nil
irb(main):004:0> puts i += 1 while i < 3
1
2
3
=> nil

Czasami będziesz chciał zanegować sprawdzany warunek. unless jest zanegowanym if, natomiast until zanegowanym while. Jeśli chcesz, poeksperymentuj z tymi instrukcjami.

Są cztery sposoby do przerywania wykonywania pętli z jej wnętrza. Pierwszy, break oznacza, tak jak w C, zupełną ucieczkę z pętli. Drugi, next, przeskakuje na początek kolejnej iteracji (podobnie jak znane z C continue). Trzeci, to specyficzne dla Rubiego redo, które oznacza ponowne wykonanie bieżącej iteracji. Następujący kod w języku C ilustruje znaczenia instrukcji break, next, i redo:

while (warunek) {
etykieta_redo:
   goto etykieta_next;        /* w Rubim: "next" */
   goto etykieta_break;       /* w Rubim: "break" */
   goto etykieta_redo;        /* w Rubim: "redo" */
   ...
   ...
etykieta_next:
}
etykieta_break:
...

Czwarty sposób by wyjść z pętli będąc w jej wnętrzu to return. Obliczenie instrukcji return spowoduje wyjście nie tylko z pętli ale również z metody która tę pętlę zawiera. Jeżeli podany został argument, będzie on zwrócony jako rezultat wywołania metody. W przeciwnym wypadku zwracane jest nil.

[edytuj] for

Programiści C mogą się zastanawiać jak zrobić pętlę "for". Petla for Rubiego może służyć w ten sam sposób, choć jest nieco bardziej elastyczna. Pętla poniżej iteruje każdy element w kolekcji (tablicy, tablicy asocjacyjnej, sekwencji numerycznej, itd.), ale nie zmusza programisty do myślenia o indeksach:

for element in kolekcja
  # ... "element" wskazuje na element w kolekcji
end

Kolekcją może być przedział wartości (to właśnie większość ludzi ma na myśli, gdy mówi o pętli for):

for num in (4..6)
  print num
end
#=> 456

W tym przykładzie przeiterujemy kilka elementów tablicy:

for elem in [100, -9.6, "zalewa"]
  puts "#{elem}\t(#{elem.class})"
end
#=> 100     (Fixnum)
#   -9.6    (Float)
#   zalewa  (String)

Ale tak naprawdę for jest po prostu innym sposobem zapisania instrukcji each, która jest naszym pierwszym przykładem iteratora. Poniższe formy są równoważne:

Jeżeli przywykłeś do C lub Javy, możesz preferować tą.

for element in kolekcja
  ...
end

Natomiast programista Smalltalka może preferować taką.

kolekcja.each {|element|
  ...
}

Iteratory często mogą być używane zamiast konwencjonalnych pętli. Jak już nabierzesz wprawy w ich użyciu, stają się zazwyczaj łatwiejsze w od pętli. Ale zanim dowiemy się więcej o iteratorach, poznajmy jedną najciekawszych konstrukcji języka Ruby: domknięcia.


[edytuj] Domknięcia i obiekty procedurowe

[edytuj] Domknięcia

Ruby jest językiem korzystającym w dużym stopniu z domknięć. Domknięcie jest blokiem kodu przekazywanym do metody. Samo w sobie nie jest obiektem. Domknięcie zawierające niewiele instrukcji, które można zapisać w jednej linii zapisujemy pomiędzy nawiasami klamrowymi ({}), zaraz za wywołaniem metody:

3.times { print "Bla" } #=> BlaBlaBla

Domknięcia dłuższe zapisujemy w bloku do ... end

i = 0
3.times do
  print i
  i += 2
end
#=> 024

Obsługa bloku przekazanego do funkcji odbywa się poprzez słowo kluczowe yield, które przekazuje sterowanie do bloku. Spójrzmy na przykład metody powtorz.

def powtorz(ilosc)
  while ilosc > 0
    yield   # tu przekazujemy sterowanie do domkniecia
    ilosc -= 1
  end
end
 
powtorz(3) { print "Bla" } #=> BlaBlaBla

Po zakończeniu wykonywania przekazanego bloku sterowanie wraca z powrotem do metody. Dzięki słowu kluczowemu yield możemy również przekazywać do bloku obiekty:

def powtorz(ilosc)
  while ilosc > 0
    yield ilosc
    ilosc -= 1
  end
end

Aby użyć wartości przekazanej do bloku stosujemy identyfikator ujęty w znaki |:

powtorz(3) { |n| print "#{n}.Bla " } #=> 3. Bla 2. Bla 1.Bla

Co jednak, gdy używamy yield, a do metody nie przekazaliśmy żadnego bloku? Aby uchronić się przed wystąpieniem wyjątku używamy metody block_given?, która zwraca true, gdy blok został przekazany.

def powtorz(ilosc)
  if block_given?
    while ilosc > 0
      yield ilosc
      ilosc -= 1
    end
  else
    puts "Brak bloku"
  end
end
 
powtorz(3) # nie przekazujemy bloku
#=> Brak bloku

[edytuj] Obiekty procedurowe

Bloki można zamienić w łatwy sposób na obiekty (są to obiekty klasy Proc. O tym, czym dokładnie są obiekty i klasy dowiesz się w rozdziale o klasach.) Można użyć w tym celu słów kluczowych lambda lub proc, z czego zalecane jest to pierwsze. Poniższy kod utworzy dwa obiekty procedurowe:

hej = lambda { print "Hej" }
 
witaj = proc do
  puts "Witaj!"
end

Aby wykonać dany blok zawarty w obiekcie procedurowym (wywołać go) należy użyć metody call:

hej.call #=> Hej
witaj.call #=> Witaj!

W wywołaniu call możemy również przekazać parametry do bloku:

drukuj = lambda { |tekst| print tekst }
drukuj.call("Hop hop!") #=> Hop hop!

Obiekty procedurowe mogą być, jak każde inne obiekty, przekazywane jako parametry. Możemy zdefiniować alternatywną metodę powtorz która bedzie wykorzystywać lambdę przekazaną jako parametr. Rozważmy poniższy przykład:

def powtorz(ile, co)
  while ile > 0
    co.call(ile) # wywołujemy blok "co"
    ile -= 1
  end
end
 
l = lambda do |x|
    print x
end
 
powtorz(3, l) #=> 321
powtorz(3, lambda { print "bla" }) #=> blablabla

Jak widzimy w ostatniej linii, obiekty lambda mogą być anonimowe (nie nadajemy im żadnej nazwy). O obiektach anonimowych dowiemy się wkrótce więcej. Natomiast w rozdziale o zmiennych lokalnych zobaczymy, że obiekty procedurowe i domknięcia zachowują kontekst (stan zmiennych lokalnych) w jakim zostały wywołane.

[edytuj] Różnice między lambdą a Proc.new

Obiekty procedurowe można również tworzyć używając konstrukcji Proc.new. Bardziej szczegółowo omówimy tę konstrukcję w rozdziale dotyczącym klas. Tutaj jedynie przedstawimy pewne różnice pomiędzy lambdami a obiektami utworzonymi za pomocą Proc.new.

Surowe obiekty Proc (ang. raw procs), czyli utworzone poprzez Proc.new, posiadają jedną niedogodność: użycie instrukcji return powoduje nie tyle wyjście z domknięcia obiektu procedurowego, co wyjście z całego bloku, w którym domknięcie było wywołane. Może to powodować niespodziewane wyniki działania naszych programów, dlatego zaleca się używanie lambd, a nie surowych obiektów Proc.

def proc1
  p = Proc.new { return -1 } 
  p.call
  puts "Nikt mnie nie widzi :-("
end
 
def proc2
  p = lambda { return -1 }
  puts "Blok zwraca #{p.call}"
end

Wywołany proc1 zwraca jedynie wartość, nie wypisze żadnego tekstu. Odmiennie działa proc2 - tutaj return powoduje, że sama lambda zwraca wartość, do której można się odwołać w dalszej części bloku, w którym utworzono lambdę.


[edytuj] Iteratory

Iteratory nie są oryginalnym pojęciem Rubiego. Występują one powszechnie w językach programowania zorientowanych obiektowo. Używane są również w Lispie, choć nie są tam nazywane iteratorami. W tym rozdziale szczegółowo przyjrzymy się wszechobecnym iteratorom Rubiego.

Czasownik "iterować" oznacza wykonywać tę samą czynność wiele razy, tak więc iterator jest czymś co wykonuje tę samą rzecz wiele razy (przykładem może być metoda powtórz z rozdziału o domknięciach).

Podczas pisania kodu potrzebujemy pętli w wielu różnych sytuacjach. W C, kodujemy je używając for lub while. Na przykład:

char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* tutaj przetwarzamy znak */
}

Składnia pętli for (...) z języka C dostarcza pewnej abstrakcji, która pomaga w utworzeniu pętli, ale sprawdzenie czy *str nie wskazuje na znak pusty znak wymaga od programisty znajomości szczegółów o wewnętrznej strukturze łańcucha znakowego. Między innymi dlatego, C jest odbierany jako język niskiego poziomu. Języki wyższego poziomu odznaczają się bardziej elastycznym wsparciem iteracji. Rozważ następujący skrypt sh powłoki systemowej:

#!/bin/sh
 
for i in *.[ch]; do
  # ... tutaj byłby kod do wykonania dla każdego pliku
done

Wszystkie pliki źródłowe i nagłówkowe języka C w bieżącym katalogu są przetwarzane i powłoka systemowa bierze na siebie detale dotyczące wskazywania i podstawiania po kolei wszystkich nazw plików, jedna po drugiej. To chyba działa na wyższym poziomie niż C, nie sądzisz?

Trzeba zauważyć jeszcze jedno: często język dostarcza iteratorów dla typów wbudowanych, ale budzi rozczarowanie gdy okazuje się, że musimy wracać z powrotem do pętli nisko poziomowych by iterować nasze własne typy danych. W programowaniu zorientowanym obiektowo (OOP - ang. Object-Oriented Programming), użytkownicy zazwyczaj definiują dużo własnych typów danych, więc to może być całkiem poważny problem.

Każdy język wspierający OOP zawiera jakieś udogodnienia dotyczące iterowania. Niektóre języki dostarczają w tym celu specjalnych klas, natomiast Ruby pozwala na definiowanie iteratorów bezpośrednio, używając w tym celu znanych już nam domknięć.

Typ String Rubiego posiada kilka użytecznych iteratorów:

ruby> "abc".each_byte { |c| printf "<%c>", c }
#=> <a><b><c>

each_byte to iterator wskazujący każdy znak w łańcuchu. Każdy znak jest podstawiany do zmiennej lokalnej c. To samo można przełożyć na coś bardziej przypominającego kod C...

s = "abc"
i = 0
while i < s.length
  printf "<%c>", s[i]
  i+=1
end
#=> <a><b><c>

... jednakże iterator each_byte jest koncepcyjnie prostszy, i wydaje się, że działałby nadal nawet gdyby klasa String uległa w przyszłości radykalnym modyfikacjom. Dużą zaletą iteratorów jest to, że zachowują one swoje poprawne działanie na przekór takim radykalnym zmianom. Jest to charakterystyczna cecha dobrego kodu w ogólności.

Innym iteratorem klasy String jest each_line.

"a\nb\nc\n".each_line { |l| print l }
#=> a
#   b
#   c

Zadania które wymagałyby dużego wysiłku w C (wyszukiwanie ograniczników linii, generowanie podłańcuchów, itd.) z użyciem iteratorów można wykonać bardzo łatwo.

Instrukcja for pojawiająca się w rozdziale o instrukcjach sterujących dokonywała iteracji przez użycie iteratora each. Iterator each klasy String działa w ten sam sposób jak each_line, więc przepiszmy powyższy przykład z for:

for l in "a\nb\nc\n"
  print l
end
#=> a
#   b
#   c

Możemy używać struktury sterującej retry w połączeniu z iterowaną pętlą. Spowoduje ona rozpoczęcie iterowania pętli od początku.

c = 0
for i in 0..4
  print i
  if i == 2 and c == 0
    c = 1
    print "\n"
    retry
  end
end
#=> 012
#   01234

Zamienienie retry na redo w powyższym przykładzie spowoduje, że tylko bieżąca iteracja będzie wykonana ponownie, z następującym wynikiem:

012
234

yield jak już wiemy, jest wyrażeniem, które przenosi sterowanie do bloku kodu który został przekazany do iteratora. Używając instrukcji yield i retry można zdefiniować iterator który będzie działał mniej więcej jak standardowa pętla while.

def WHILE(warunek)
  return if not warunek
  yield
  retry
end
 
i=0
WHILE(i < 3) { print i; i+=1 } #=> 012

Jak więc widzimy, iteratory w Rubim są metodami obsługującymi przekazane do nich domknięcia. Owszem, istnieją pewne ograniczenia, ale możesz pisać własne oryginalne iteratory. Szczególnie, gdy definiujemy nowy typ danych, wygodnie jest zdefiniować odpowiednie iteratory które będą na nim operować. W tym kontekście powyższe przykłady nie są szczególnie użyteczne. Poznajmy zatem bardziej praktyczne iteratory.

[edytuj] Przegląd iteratorów

[edytuj] all?

Przekazuje do bloku każdy element kolekcji. Zwraca true, jeśli blok nigdy nie zwróci false (lub nil).

[1, 2, 5].all? { |element| element <= 5 }                     #=> true
[1, 2, 5].all? { |element| element <= 4 }                     #=> false
%w{RAM CPU GPU DDR}.all? { |element| element.length == 3 }    #=> true
 
[1, 2, 5].all? do |element|
  puts "Sprawdzam #{element}; #{element} < 5: #{element < 5}"
  element < 5
end                                                           #=> false

Wyjście:

Sprawdzam 1; 1 < 5: true
Sprawdzam 2; 2 < 5: true
Sprawdzam 5; 5 < 5: false

[edytuj] any?

Zwraca true, jeśli przekazany do bloku element kiedykolwiek zwróci true.

[1, 2, 5].any? { |element| element > 5 }    # => false
[1, 2, 5].any? { |element| element == 2}    # => true

[edytuj] collect(map)

Przekazuje do bloku każdy element kolekcji, następnie tworzy nową - z elementów zwracanych przez blok.

%w{kot tulipan parowka}.collect { |element| element.upcase }
# => ["KOT", "TULIPAN", "PAROWKA"]
[1, 2, 3].collect { |element| element + 1}
#=> [2, 3, 4]

[edytuj] collect!(map!)

Działa jak collect, z tą jednak różnicą, że operacji kolekcja dokonuje na sobie, w każdej iteracji zmieniając swoją zawartość.

a = [1, 2, 3]                            #=> [1, 2, 3]
a.collect! { |element| element + 1 }     #=> [2, 3, 4]
a                                        #=> [2, 3, 4]

[edytuj] delete_if

Usuwa z kolekcji elementy, dla których blok zwraca true

[1, 2, 3, 4, 5, 6].delete_if { |i| i%2 == 0 }   # => [1, 3, 5]

[edytuj] detect(find)

Zwraca pierwszy element, dla którego blok zwróci true

(36..100).detect { |i| i%7 == 0 }     # => 42

[edytuj] downto

Wykonuje blok, podając w kolejności malejącej liczby od siebie samej do podanej jako parametr.

9.downto(0) { |i| print i } #=> 9876543210

[edytuj] each

Przekazuje do bloku każdy z elementów kolekcji

['pies', 'kot', 'ryba'].each { |word| print word + " " }
(0..9).each { |i| print i }
#=> pies kot ryba 0123456789

[edytuj] each_index

Działa jak each, ale przekazuje sam indeks każdego elementu.

[3, 6, -5].each_index { |i| print i.to_s + " " }
#=> 0 1 2

[edytuj] each_with_index

Przekazuje jednocześnie element i jego indeks do bloku.

["jeden", 2, "trzy"].each_with_index do |element, index|
  puts "Indeksowi #{index} przyporzadkowalem #{element}"
end
 
#=> Indeksowi 0 przyporzadkowalem jeden
#   Indeksowi 1 przyporzadkowalem 2
#   Indeksowi 2 przyporzadkowalem trzy

[edytuj] find_all

Zwraca wszystkie elementy kolekcji, dla których blok zwróci true.

(0..30).find_all { |i| i%9 == 0 }     #=> [0, 9, 18, 27]

[edytuj] grep

Zwraca elementy spełniające dopasowanie podane jako parametr. Jeśli podano blok, przekazuje do niego tylko te elementy i zwraca tablicę zbudowaną z wartości zwracanych przez blok.

# Zwraca wyrazy zawierajace litere 'r'
%w{ruby python perl php}.grep(/r/) do |w| 
  print "#{w.upcase} "
  w.capitalize
end                         #=> ["Ruby", "Perl"]
 
#=> RUBY PERL

[edytuj] inject

Przekazuje do bloku każdy element kolekcji. Posiada dodatkowo pamięć, która początkowo jest równa pierwszemu elementowi (lub wartości podanej jako parametr). Po zakończeniu każdej iteracji pamięć jest aktualizowana do wartości zwracanej przez blok.

# Zwraca największą liczbę z tablicy
a = [-5, 2, 10, 17, -50]
a.inject a.first do |mem, element|
  mem > element ? mem : element
end                                 #=> 17
 
# Silnia
(1..5).inject do |mem, element|
  mem *= element
end                                 #=> 120

[edytuj] partition

Zwraca dwie tablice: jedną z elementami, dla których blok zwraca true i drugą - z resztą.

(1..6).partition { |i| i%2 == 0 }   #=> [[2, 4, 6], [1, 3, 5]]

[edytuj] reject

Odrzuca z kolekcji wszystkie elementy, dla których blok zwróci true.

(1..10).reject { |i| i >= 3 and i <= 7 }    #=> [1, 2, 8, 9, 10]

[edytuj] reject!

Wyrzuca z siebie elementy, dla których blok zwraca true.

a = (1..10).to_a                     # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a.reject! { |i| i >= 3 and i <= 7 }  # => [1, 2, 8, 9, 10]
a                                    # => [1, 2, 8, 9, 10]

[edytuj] reverse_each

Działa jak each tyle, że podaje elementy w odwrotnej kolejności.

(0..9).to_a.reverse_each { |i| print i }
#=> 9876543210

[edytuj] step

Przekazuje do bloku wartości od, do - z określonym krokiem.

# (1)
0.step(100, 10) { |i| puts i}
# (2)
(0..100).step(10) { |i| puts i }

W obu przypadkach wyjście będzie wyglądało tak:

0
10
20
30
40
50
60
70
80
90
100

[edytuj] times

Wykonuje dany blok określoną ilość razy.

5.times { puts "Hej!" }
5.times { |i| print "#{i} "}
 
#=> Hej!
#   Hej!
#   Hej!
#   Hej!
#   Hej!
#   0 1 2 3 4

[edytuj] upto

Iteruje blok, przekazując liczby od, do.

1.upto(3) { |i| print i }
#=> 123


[edytuj] Programowanie zorientowane obiektowo

[edytuj] Myślenie zorientowane obiektowo

Zorientowany obiektowo to chwytliwe określenie. Powiedzenie o czymkolwiek, że jest "zorientowane obiektowo" brzmi naprawdę mądrze. Ruby określa się jako język skryptowy zorientowany obiektowo, ale co to naprawdę znaczy "zorientowany obiektowo"?

Istnieje wiele różnorodnych odpowiedzi na to pytanie, które można by prawdopodobnie sprowadzić do tego samego. Zamiast jednak zbyt szybko podsumowywać to zagadnienie, spróbujmy pomyśleć przez chwilę o tradycyjnym paradygmacie programowania.

Tradycyjnie, problem programowania rozwiązywany jest przez podejście, w którym obecne są różne typy reprezentacji danych oraz procedury które na tych danych operują. W modelu tym dane są obojętne, pasywne i bezradne; oczekują na łaskę dużej proceduralnej bryły, która jest aktywna, logiczna i wszechmocna.

Problem w tym podejściu jest taki, że programy są pisane przez programistów, którzy są tylko ludźmi i potrafią pamiętać i kontrolować w danym czasie tylko pewną skończoną ilość detali. Jak projekt staje się większy jego proceduralny rdzeń rośnie do punktu, w którym trudno jest już pamiętać jak cała rzecz działa. Drobne pomyłki w sposobie myślenia i usterki typograficzne stają się przyczyną dobrze zamaskowanych błędów. Złożone i niezamierzone interakcje zaczynają wynurzać się z proceduralnego rdzenia i zarządzanie tym zaczyna przypominać noszenie w kółko wściekłej kałamarnicy i walkę z jej mackami. Są pewne wytyczne dotyczące programowania, które mogą pomóc zminimalizować i zlokalizować błędy w tym tradycyjnym paradygmacie, ale jest lepsze rozwiązanie które pociąga za sobą fundamentalną zmianę sposobu pracy.

Programowanie zorientowane obiektowo pozwala nam przekazywać większość przyziemnych i monotonnych czynności logicznych do samych danych; zmienia to nasze pojmowanie danych z pasywnych na aktywne. Innymi słowy:

  • Przestajemy traktować każdy kawałek danych jak skrzynkę z otwartym wiekiem, do której możemy wkładać lub wyjmować przedmioty.
  • Zaczynamy traktować każdy kawałek danych jak pracującą maszynę, z zamkniętą pokrywą i dobrze oznakowanymi przełącznikami oraz potencjometrami.

To co nazwaliśmy wyżej "maszyną" może być w środku bardzo proste lub bardzo złożone. Nie możemy tego określić patrząc z zewnątrz, jak i nie grzebiemy w jej wnętrzu (za wyjątkiem sytuacji, gdy jest absolutnie pewnie, że coś jest nie tak z jej projektem), więc jedyne czego się od nas wymaga by wpływać na dane to przekręcanie przełączników i odczytywanie potencjometrów. Jak już maszyna jest zbudowana nie chcemy sobie dalej zaprzątać głowy tym, jak ona działa.

Możesz sądzić że po prostu robimy sobie więcej roboty, ale tak naprawdę to dobra robota w celu chronienia wielu rzeczy przed błędami.

Zacznijmy od przykładu, który jest zbyt prosty by mieć wartość praktyczną, ale powinien zilustrować przynajmniej jedną cześć tej koncepcji. Twój samochód ma tripmeter[2]. Jego celem jest rejestrowanie odległości którą przebył pojazd od momentu naciśnięcia przycisku. Jak moglibyśmy wymodelować to w języku programowania? W C, tripmeter byłby po prostu zmienną numeryczną, możliwe że typu float. Program mógłby manipulować tą zmienną zwiększając jej wartość przyrostowo małymi krokami, z okazjonalnym resetowaniem jej wartości do zera, jeśli zaszłaby taka potrzeba. A co w tym złego? Z nieokreślonej liczby niespodziewanych powodów błąd w programie mógłby przypisać błędną wartość do zmiennej. Każdy, kto programował w C wie, co to znaczy spędzać godziny lub dni próbując ustalić gdzie tkwi taki błąd. Jego przyczyna, jak się już ją odkryje, wydaje się absurdalnie głupia. (Moment znajdowania błędu jest przeważnie rozpoznawalny przez odgłos głośnego klepnięcia w czoło.)

Ten sam problem można zaatakować z zupełnie innej strony, w podejściu zorientowanym obiektowo. Pierwszym pytaniem, które zadaje programista, gdy projektuje tripmeter nie jest "jaki znany mi typ danych odpowiada najbliżej tej rzeczy?" ale "jak właściwie ta rzecz ma działać?" Różnica jest zasadnicza. Potrzeba poświęcić odrobinę czasu ustalając po co dokładnie jest drogomierz i w jaki sposób zewnętrzny świat zamierza się z nim kontaktować. Decydujemy się zbudować małą maszynkę z metodami regulacji które pozwolą nam zwiększać wartość, czytać ją, kasować, i nic poza tym.

Nie dostarczamy żadnego sposobu na przypisanie do tripmetera arbitralnych wartości. Dlaczego? Ponieważ wszyscy wiemy, że drogomierze nie działają w ten sposób. Jest tylko kilka rzeczy, które powinieneś robić z tripmeterem, i to wszystko na co pozwalamy. Zatem, jeśli coś innego w programie błędnie spróbuje umieścić jakąś inną wartość (powiedzmy, docelową temperaturę ze systemu sterowania klimatyzacją w samochodzie) w tripmeterze, dostaniemy natychmiastową wskazówkę co poszło nie tak. Będziemy poinformowani, gdy program zostanie uruchomiony (lub podczas kompilacji, zależnie od natury języka programowania), że nie mamy prawa przypisywać arbitralnych wartości do obiektów Tripmeter. Wiadomość może nie będzie dokładnie tak jasna, ale będzie ona sensownie zbliżona. Nie uchroni nas to przed błędem, prawda? Ale za to szybko wskaże nam, gdzie mniej więcej leży przyczyna błędu. To tylko jeden z kilkunastu sposobów, w jaki OOP może nam zaoszczędzić dużo zmarnowanego czasu.

Zazwyczaj robimy jeszcze jeden krok w abstrakcji, ponieważ okazuje się, że równie łatwo jak naszą maszynę można zbudować całą fabrykę która tworzy takie maszyny. Prawdopodobnie nie budowalibyśmy bezpośrednio pojedynczego tripmetera, raczej zbudowalibyśmy dowolną ilość tripmeterów z pojedynczego wzorca. Ten wzorzec (lub jak wolisz, fabryka tripmeterów) odpowiada temu co nazywamy klasą. Indywidualny tripmeter wygenerowany z tego wzorca (lub zbudowany przez fabrykę) odpowiada obiektowi. Większość języków obiektowych, wymaga by klasa była zdefiniowana nim będziemy mogli uzyskać nowy rodzaj obiektu, ale nie Ruby.

Warto tu zanotować, że użycie języka zorientowanego obiektowo nie wymusza odpowiedniego zorientowanego obiektowo projektu. W rzeczy samej, pisanie kodu, który jest niejasny, niechlujny, źle zaczęty, pełny błędów i chwiejący się na wszystkie strony, możliwe jest w każdym języku. To co Ruby robi dla ciebie (szczególnie w przeciwieństwie do C++) to to, że praktyka programowania obiektowego jest na tyle naturalna, że nawet gdy pracujesz w małej skali nie czujesz potrzeby by uciec się do brzydkiego kodu by zaoszczędzić sobie wysiłku. Będziemy omawiać sposoby, w których Ruby osiąga ten wspaniały cel, w miarę postępu tego podręcznika. Następnym tematem będą "przełączniki i potencjometry" (metody obiektów) a stamtąd przeniesiemy się do "fabryk" (klas). Jesteś wciąż z nami?


[edytuj] Metody

[edytuj] Czym jest metoda?

W programowaniu obiektowym nie myślimy o operowaniu na danych bezpośrednio spoza obiektu. Obiekty mają raczej pewne rozumienie tego jak należy operować na sobie samych (gdy ładnie poprosimy by to robiły). Można powiedzieć, że przekazujemy pewne wiadomości do obiektu i te wiadomości zazwyczaj powodują jakiegoś rodzaju akcję lub uzyskują znaczącą odpowiedź. Powinno to dziać się bez naszej zaangażowania w to, jak obiekt naprawdę działa od wewnątrz. Zadania, o wykonanie których mamy prawo prosić obiekt (lub równoważnie - wiadomości które on zrozumie) są właśnie owymi metodami obiektu.

W Rubim wywołujemy metody obiektu posługując się zapisem z kropką (tak jak w C++ lub Javie). Nazwa obiektu do którego mówimy znajduje się na lewo od kropki.

"abcdef".length #=> 6

Rozumując intuicyjnie, ten łańcuch pytany jest o swoją długość. Technicznie natomiast, wywołujemy metodę length na rzecz obiektu "abcdef".

Pozostałe obiekty mogą mieć nieco inną interpretację długości lub nawet nie mieć żadnej. Decyzje dotyczące tego, jak odpowiedzieć na wiadomość podejmowane są w locie, podczas wykonywania programu, i podejmowane działanie może być zmienione w zależności o tego, na co wskazuje zmienna.

a = "abc"
puts a.length #=> 3
a = ["abcde", "fghij"]
a.length #=> 2

To, co rozumiemy przez długość może się różnić w zależności od rodzaju obiektu do którego mówimy. Za pierwszym razem w powyższym przykładzie pytamy a o jej długość i a wskazuje na prosty łańcuch znakowy, więc jest tylko jedna sensowną odpowiedź. Za drugim razem a odnosi się do tablicy i możemy rozsądnie myśleć o jej długości jako o 2, 5 lub 10 - oczywiście w tym przypadku jest to 2. Różne obiekty mogą mieć różnego rodzaju długości.

a[0].length #=> 5
a[0].length + a[1].length #=> 10

Rzeczą godną uwagi jest to, że tablica wie o sobie to coś, co oznacza, że jest ona tablicą. W Rubim kawałki danych przechowują tę wiedzę. Zatem żądania, które wobec nich kierujemy mogą być automatycznie spełnione na wiele różnych sposobów. Zdejmuje to z programisty brzemię pamiętania olbrzymiej liczby wielu specyficznych nazw funkcji, ponieważ relatywnie mała liczba nazw metod (będących w zgodzie koncepcjami wyrażalnymi w języku naturalnym) może być zastosowana do różnych typów danych. W rezultacie programista otrzymuje to, czego się spodziewał. Ta cecha języków programowania obiektowego nazywana jest polimorfizmem.

Kiedy obiekt otrzymuje komunikat, którego nie rozumie, "podnoszony" jest błąd:

a = 5
a.length
ERR: (eval):1: undefined method `length' for 5(Fixnum)

Tak więc należy wiedzieć, które metody są akceptowane przez obiekt, chociaż nie trzeba analizować jak są one przetwarzane.

Jeżeli przekazujemy do metody jakieś argumenty, zazwyczaj otaczamy je nawiasami okrągłymi:

obiekt.metoda(arg1, arg2)

Można je pominąć, jeśli nie stanie się to przyczyną dwuznaczności[3].

obiekt.metoda arg1, arg2

Jest pewna specjalna zmienna w Rubim - self. Odnosi się ona tylko do obiektu na rzecz którego wywołujemy metodę. Dzieje się to tak często, że dla wygody "self." może być opuszczone w metodach odwołujących się z danego obiektu do samego obiektu:

self.nazwa_metody(argumenty...)

oznacza to samo co

nazwa_metody(argumenty...)

To co tradycyjnie nazwalibyśmy wywołaniem funkcji jest po prostu skróconą formą zapisu wywołań metod przez self. To właśnie czyni z Rubiego czysto obiektowy język programowania. Ponadto metody funkcyjne nadal zachowują się całkiem podobnie do funkcji w innych językach programowania. Jest to pewne ułatwienie dla tych, którym łatwiej jest traktować wywołania metod jak wywołania funkcji. Jeśli chcemy, możemy, np. w celach edukacyjnych, traktować funkcje tak jakby nie były one naprawdę metodami obiektów.

W rozdziale dotyczącym zmiennych klasowych zobaczymy zastosowanie słowa kluczowego self przy definiowaniu metod należących do całej klasy, czyli metod klasowych.

[edytuj] * czyli zmienna lista argumentów

Czasami, analizując różne przykłady kodu w Rubim możemy natknąć się na taką definicję metody (albo wywołanie), w której ostatni parametr poprzedzony jest znakiem * lub &. Dla początkujących może to wyglądać enigmatycznie, a programistów C/C++/C# mogą dodatkowo mylić skojarzenia ze wskaźnikami i referencjami. Obydwa znaki mają jednak zupełnie inne znaczenie, a ponieważ nie ma nic gorszego od kodu, którego nie rozumiemy, wyjaśnijmy znaczenie obu tych symboli.

Gwiazdka (*) oznacza zmienną listę argumentów. Jeżeli * pojawia się w nagłówku definiowanej metody, poprzedzając ostatni parametr, oznacza to, że począwszy od tego argumentu do metody można przekazać dowolną ich ilość. Wszystkie te argumenty są widoczne w metodzie jako tablica.

def metoda(*args)
  wynik = ""
  args.each {|arg| wynik += "#{arg}, "}
  wynik[0...-2] # ucinamy 2 ostanie znaki: ", "
end
 
puts metoda("a", "b", 3) #=> a, b, 3

Gwiazdkę * można też stosować w wywołaniu metody, przed ostatnim argumentem - tablicą. Powoduje ona wtedy konwersję z tablicy na poszczególne argumenty:

def inna_metoda(a, b, c)
  "#{a}, #{b}, #{c}"
end
 
puts inna_metoda(*["a", "b", 3]) #=> a, b, 3
puts inna_metoda("a", *["b", 3]) #=> a, b, 3

[edytuj] & czyli przekazywanie bloku

Poznaliśmy już domknięcia i sposoby przekazywania ich do metody. Domknięcie możemy przekazać, definiując je bezpośrednio za nazwą metody. Natomiast obiekt procedurowy możemy przekazywać jako parametr. Wiemy też, że sterowanie do domknięcia przekazujemy przez yield, natomiast procedurę obiektu procedurowego wywołujemy przez metodę call. Co jednak, gdy chcielibyśmy użyć bloku przekazanego jako domknięcie tak jakby był obiektem (stosując call zamiast yield)? Albo gdybyśmy chcieli utworzony już obiekt procedurowy przekazać tak jakby był blokiem?

Rozważmy naszą metodę powtorz z rozdziału o domknięciach:

def powtorz(ilosc)
  while ilosc > 0
    yield ilosc
    ilosc -= 1
  end
end

Aby przekazać do tej metody blok, który mamy w postaci np. lambdy, należy użyć symbolu & i przekazać nasz blok jako ostatni (niby fikcyjny) argument. Fikcyjny, bo nie jest on jawnie zdefiniowany w nagłówku metody.

l = lambda { |x| print x }
powtorz(3, &l) #=> 321

Efekt jest taki sam jakbyśmy przekazali blok tradycyjnie:

powtorz(3) { |x| print x } #=> 321

Symbolu & możemy te