Ruby/Powrót do prostych przykładów

Powrót do prostych przykładów edytuj

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.

Silnie edytuj

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ć?

Łańcuchy znakowe edytuj

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ć.

slowa = ['fiolek', 'roza', 'bez']
sekret = slowa[rand(3)]

print "zgadnij? "
while odp = STDIN.gets
  odp.chop!
  if odp == sekret
    puts "Wygrales!"
    break
  else
    puts "Przykro mi, przegrales."
  end
  print "zgadnij? "
end
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".

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

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

Wyrażenia regularne edytuj

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

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

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:

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

lub w "destruktywnym" stylu:

tekst.gsub!(wyr,"#{st}\\&#{en}")
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.