Wyrażenia w Rubim

Posted by Jacek Galanciak on

W języku Ruby niemal każda konstrukcja, z którą możemy się zetknąć, to wyrażenie - a to zwraca, jak wiemy, określoną wartość. Taka właściwość daje szereg dodatkowych możliwości - ale i obciąża programistę, który niekiedy musi przez to nieco zmienić swoje podejście do kodu i algorytmów. Nic nie stoi na przeszkodzie, by pisać w Rubim z klasyczną manierą, znaną z języków średniego poziomu - ale czy jest sens marnować tak potężne właściwości języka?

Przyjrzyjmy się więc bliżej niektórym rodzajom wyrażeń.

Przypisania

Wykonajmy parę przypisań w interaktywnej konsoli Rubiego. Uznałem, że pokazanie dokładnego przebiegu sesji irb-a powoduje, ze kod traci na czytelności, dlatego wstawiam go w “czystej” postaci, a wartości zwracane przez dane wyrażenie zaznaczam w komentarzach.

a = 1 # => 1
s = "kot" # => "kot"
arr = [1, 3.5, "tekst"] # => [1, 3.5, "tekst"]

Widzimy, że już samo przypisanie zwraca wartość. Wykorzystać to możemy w sposób oczywisty:

a = b = 3 # => 3
                        # Wynik: a = 3, b = 3
x = (y = 2 + 5) + 1 # => 8
                        # Wynik: x = 8, y = 7

Przypisanie tego rodzaju polega na przypisaniu obiektowi referencji do zmiennej bądź stałej. Kiedy zagłębimy się w programowanie obiektowe w Rubim, poznamy drugi rodzaj, który tak naprawdę jest metodą, odwołującą się do np. atrybutu klasy czy indeksu kolekcji.

Przypisania równoległe

a, b = 2, 1 # [2, 1]
                    # Wynik: a = 2, b = 1

Okazało się, że operacja przypisywania “jednym ciurkiem” zwraca tablicę. Przyjrzyjmy się bliżej tej właściwości:

a = [1, 2 ,3 ,4, 5] # => [1, 2, 3, 4, 5]
b, c, *d = a # => [1, 2, 3, 4, 5]
                        # Wynik: b = 1, c = 2,
                        # d = [3, 4, 5]

Taka operacja powoduje przypisanie pierwszemu elementowi lewej strony pierwszy element tablicy, drugiemu - drugi, a ostatniemu, poprzedzonemu gwiazką, całą resztę. Prawda, że ciekawe?

Zamiana wartości dwóch zmiennych

W klasycznym podejściu do programowania angażujemy w tę operację trzecią, tymczasową zmienną. W Rubim sprawa wygląda dużo prościej (i bardziej czytelnie):

a, b = b, a

++

W Rubim nie mamy operatora ++, znanego z wielu innych języków programowania. Zamiast niego stosujmy +=, niekoniecznie z wartością 1 :-)

Natura wyrażeń w Rubim

Każda jednostkowa operacja zwraca wartość, która jest obiektem, a ten posiada metody, które wykonać, otrzymując kolejną wartość-obiekt, którą…

[1, 2, 3, 2, 1, 2, 3, 5, 8].uniq.sort.reverse # => [8, 5, 3, 2, 1]

Powyższy kod wyrzuca duplikaty z tablicy, następnie ją sortuje, by ostatecznie odwrócić kolejność jej elementów.

Uwaga na zera i puste Stringi!

Na tym bardzo łatwo wpaść, jeśli przyzwyczajeni jesteśmy do definicji fałszu wg. takich języków jak PHP czy C. Okazuje się, że fałszem w Rubim jest

  • nil
  • false

i nic więcej. Zapamiętajmy więc, że ani zero, ani pusty String nie są fałszem. Bo nie są - one przecież istnieją! :-)

if 0 
  puts "Zero jest prawdą!"
end
a = ""
if a
  puts "Pusty String też!"
end

Operatory logiczne

Znane nam operatory and, or oraz not posiadają w Rubim szereg ciekawych właściwości.

Operator and oraz && (różniące się tylko priorytetem - and ma niższy) jest dwuargumentowy:

wyr1 && wyr2

lub też

wyr1 and wyr2

Działa następująco:

  • sprawdza, czy wyr1 jest prawdą - jeśli nie, zwraca wyr1 i kończy działanie; wyr2 jest wtedy pomijane i w ogóle nie jest wykonywane
  • jeśli wyr1 jest prawdą, sprawdza, czy wyr2 jest prawdą; jeśli jest - zwraca wyr2
  • jeśli jednak nie jest - również zwraca wyr2

Brzmi trochę skomplikowanie, jak na zwykły operator logiczny, który wielu osobom znany jest z zer i jedynek. Niech parę przykładów rozjaśni całą sytuację:

false and nil # => false
nil and false # => nil
false and 1 # => false
1 and nil # => nil
1 and false # => false
1 and "tekst" # => "tekst"
"tekst" and 1 # => 1
"aaa" and "bbb" # => "bbb"

Zarówno sposób użycia operatorów or oraz ||jak i ich priorytet, wygląda dokładnie tak samo, jak w przypadku and. Działanie operatora or przedstawia się następująco:

  • jeśli wyr1 jest prawdą, zwraca wyr1 i kończy działanie; wyr2 nie jest wtedy w ogóle wykonywane
  • jeśli wyr1 jest fałszem, sprawdza wyr2; jeśli jest prawdą, zwraca wyr2
  • jeśli jednak jest fałszem, również zwraca wyr2

Parę przykładów:

true or false # => true
true or "a" # => true
"a" or 5 # => "a"
5 or "a" # => 5
false or nil # => nil
nil or false # => false

Działanie jednoargumentowego operatora not oraz ! jest dużo prostsze: zwraca fałsz, gdy argument jest prawdą, i prawdę, gdy argument jest fałszem. Tak jak poprzednio, not ma priorytet niższy niz !. Oto parę przykładów:

not false # => true
not nil # => true
not 4 # => false
not 3.14 # => false

Zwróćmy uwagę, że not zwraca jedynie wartości true oraz false. Zanegowany String daje false, ale nie jest to operacja zalecana (wyskakuje warning), dlatego nie będziemy jej wykonywać.

Operator ||=

Działa w dość ciekawy sposób, skracając nieco zapis pewnych instrukcji.

cart ||= Cart.new

Kod ten stworzy nowy koszyk, ale tylko wtedy, gdy jeszcze takowego nie stworzyliśmy (czyli: jeśli cart jest fałszem).

Operatory porównania

Większą część znamy z innych języków programowania, dlatego ich działanie przedstawione będzie w skrócie:

  • ==, <, >, <=,>= nie wymaga komentarza
  • == posiada swoje zaprzeczenie !=
  • <=>, zwany ogólnym operatorem porównania, zwraca -1, 0 1, gdy wartość lewego operandu jest odpowiednio: mniejsza, równa, większa od wartości drugiego operandu; służy do implementacji wyrażenia case
  • =~ sprawdza dopasowanie wzorca wyrażeń regularnych
  • eql? zwraca prawdę, jeżli oba operandy są tej samej wartości oraz tego samego typu
  • equal? zwraca prawdę, jeśli pierwszy operand jest dokładnie tym samym, co drugi - czyli czy posiada to samo ID
  • wszystkie wymienione wyżej operatory (poza !~ oraz !=, które są konwertowane w czasie wykonywania kodu odpowiednio na !(a == b) oraz !(a =~ b)) to metody , które można nadpisywać

Jak używać operatorów eql? i equal?

(2.0).eql?(2) # => false
str = "tekst"
str.eql? "tekst" # => true; zwrócmy uwagę, że nie trzeba 
                 # używać nawiasów
str2 = str
str.equal? str2 # true
str.object_id # 24327300
str2.object_id # 24327300

Widać tutaj, że = jest przypisaniem referencji , a nie faktycznym podstawieniem wartości. Może to czasem powodować trudne do wykrycia błędy. Użycie str2 = str.dup spowoduje przypisanie str2 wartości str, ale będą to różne obiekty (różne object_id => equal? zwróci false).

Wyrażenia regularne

W zasadzie jest to temat nie tylko na osobny wpis, ale i na osobną książkę. Tutaj przedstawiony będzie sam fakt ich istnienia :-).

Ruby ma wbudowaną obsługę tego potężnego narzędzia do pracy z tekstem - jest to element jego natywnej składni, a nie bibliotek. Dopasowanie do wzorca sprawdzamy za pomocą operatora =~, a same wyrażenia definiujemy jako sekwencję odpowiednich znaków między ukośnikami: /regexp/. A jak to wygląda w praktyce?

if str = gets =~ /R(uby|ails)/
    puts "Ha! Wiesz co dobre!"
else
    puts "Buuu!"
end

To nawet nie jest smaczek tego, co oferuje nam Ruby i jego wyrażenia regularne. Program wczytuje ze standardowego wejścia tekst. Jeśli zawiera on słowo Ruby lub Rails, wypisze odpowiedni tekst pochwalny.

Podsumowanie

Wygląd i charakter wyrażeń jest jedną z cech rozpoznawczych języka Ruby. Zachęcam do zapoznania się z nimi w sposób bardziej zaawansowany, bo tylko wtedy wykorzystamy cały potencjał drzemiący w tym języku.