Bloki, domknięcia i yield w Rubim
Posted by Jacek Galanciak on Jul 8 2007
W tej części tutoriala poruszymy bez wątpienia jeden z ważniejszych elementów składni Rubiego. Dzięki tej wiedzy z łatwością unikniemy powtórzeń kodu, a także będziemy mogli implementować własne iteratory, co uczynimy na końcu tutoriala.
begin
i end
Klamra zbudowana z tych dwóch słów kluczowych służy do grupowania instrukcji. Stosuje się je do obsługi wyjątków oraz modyfikatorów instrukcji warunkowych.
Spójrzmy na działanie klamry begin
- end
na zasięg zmiennych:
begin
a = 10
end
puts a
Program na wyjściu da nam 10. Zadeklarowanie zmiennej w takiej klamrze nie spowoduje , że będzie ona lokalna i istniała tylko w bloku begin
- end
.
Dla porównania, spójrzmy na poniższy przykład:
loop do
a = 10
break
end
puts a
W tym przypadku otrzymamy błąd: undefined local variable or method `a' for main:Object (NameError).
Błąd otrzymamy także w kodzie:
begin
break
end
unexpected break (LocalJumpError)
Tak, jak pisałem w poście o pętlach, modyfikatory wyrażeń warunkowych i pętli można stosować do więcej niż jednej instrukcji - pod warunkiem, że się je odpowiednio zgromadzi:
a = b = 0
begin
a += 1
b += 0.1
end until a > 10
yield
Bardzo dziwne, ale użyteczne słowo. Będzie się dość często przewijać przez dalszą część tego tutoriala i przez cały czas trwania Waszej rubinowej kariery.
Przyjrzyjmy się przykładowi niżej:
def twice
yield
yield
end
# 1
twice do
puts "Hello!"
end
# 2
twice { puts "Hello!" }
Instrukcja yield
wykonuje blok, który został przekazany do metody. Blok przekazujemy poprzez do
- end
lub nawiasy klamrowe. Drugi przypadek stosuje się często do małych bloków. Jeżeli są dłuższe niż jedna instrukcja, a całość chcemy zmieścić w jednej linii kodu, instrukcje oddzielamy średnikami.
Wywołajmy twice
bez żadnego bloku.
twice
Otrzymamy błąd in `twice': no block given (LocalJumpError). Wypadało by się przed czymś takim zabezpieczyć.
def twice
if block_given?
yield
yield
else
puts "Dzisiaj mam wolne :)"
end
end
twice do
puts "Hello"
end
twice
Metoda block_given?
pozwala na sprawdzenie, czy metodzie przekazano blok. Program w takiej wersji da na wyjściu:
Hello
Hello
Dzisiaj mam wolne :)
Przekazywanie obiektów do bloku
Nie raz już przekazywaliśmy obiekty do bloku, umieszczając go między znakami |
. Teraz napiszemy własną metodę, która to zrealizuje:
def desc
a = 1;
10.times do
a *= 0.5
yield a if block_given?
end
return a
end
puts desc # 1
desc do |val| # 2
print "Iteracja: #{(-Math.log(val)/Math.log(2)).to_i}: "
puts val
end
0.0009765625
Iteracja: 1: 0.5
Iteracja: 2: 0.25
Iteracja: 3: 0.125
Iteracja: 4: 0.0625
Iteracja: 5: 0.03125
Iteracja: 6: 0.015625
Iteracja: 7: 0.0078125
Iteracja: 8: 0.00390625
Iteracja: 9: 0.001953125
Iteracja: 10: 0.0009765625
Metodę desc
wywołaliśmy na dwa sposoby - z blokiem i bez niego.
proc
i lambda
Mówi się, że wszystko w Rubim jest obiektem. Nie jest to do końca prawdą, gdyż blok nim nie jest. Z łatwością można go jednak skonwertować na obiekt klasy Proc
.
Blok sam w sobie nie może istnieć. Zarówno pierwszy, jak i drugi przypadek zwróci błąd.
{ puts "Hello" } # 1
do # 2
puts "Hello"
end
Z łatwością możemy jednak stworzyć lambdę, czyli blok kodu przechowany w obiekcie, by potem wywołać ją poprzez metodę call
.
p1 = lambda do
puts "Hello"
end
p1.class # => Proc
Można użyć także metody proc
, która jest synonimem lambda
, ale bardziej zaleca się stosowanie lambda
.
Lambdy a kontekst bloku
Lambdy posiadają niezwykle ciekawą właściwość: pamiętają kontekst, w jakim zostały wywołane. W praktyce oznacza to, że pamiętają wartości swoich zmiennych i ponowne wywołaniebloku ich nie wyzeruje - wprost przeciwnie, lambda będzie kontynuować swoje działanie, korzystają z takich wartości swoich zmiennych, z jakimi je ostatnio porzuciła.
Napiszemy teraz program, który stworzy trzy “podprocesy” o takiej samej treści, które pokażą, że każdy z osobna będzie pamiętał swoje własne zmienne.
def proc1
val = "default"
lambda do
puts "Poprzednia wartosc: #{val}. Podaj wartosc aktualna:"
val = gets
end
end
p1 = proc1
p2 = proc1
p3 = proc1
procs = [p1, p2, p3]
loop do
3.times do |i|
print "procs[#{i}]: "
procs[i].call
end
end
Warto zajrzeć do komentarzy wpisu o wejściu i wyjściu, które zainspirowały mnie do rozwinięcia tego właśnie akapitu.
Proc.new
a lambdy
Okazuje się, że jest jeszcze trzeci sposób tworzenia obiektów Proc
:
p1 = Proc.new { puts "Hello" }
p1.call
Jedną z różnic jest fakt, ze Proc.new
nie sprawdza zgodności ilości argumentów, jaką przekazujemy do bloku.
Kolejna różnica przesądza o fakcie, że bardziej zaleca się używanie lambd.
Instrukcja return
a obiekty Proc
Surowe obiekty Proc
(ang. raw procs), czyli utworzone poprzez Proc.new
, posiadają jedną niedogodność: użycie instrukcji return
powoduje nie tyle wyjście z proca, co wyjście z całego bloku, w którym proc był wywołany. Może to powodować niespodziewane wyniki działania naszych programów, dlatego zaleca się używanie lambd, a nie surowych proców.
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 labda zwraca wartość, do której można się odwołać w dalszej części bloku, w którym utworzono lambdę.
Przykłady
Wynajdziemy teraz na nowo koło i napiszemy własną wersję instrukcji until
wykorzystując naszą wiedzę o yieldach.
def _until(cond)
return if cond
yield
retry
end
a = 0
_until a == 5 do
print a, " "
a += 1
end
Wyjście: 0 1 2 3 4
Napiszmy teraz własną wersję metod all?
(zwraca true
, jeśli wszystkie elementy kolekcji powodują, że dany blok zwraca true
) oraz any?
(zwraca true
, jeśli którykolwiek z elementów …) [polecam przejrzenie wpisu o iteratorach i implementację paru innych w ramach ćwiczenia]:
class Array
def _all?
self.each do |element|
return false if not yield element
end
return true
end
def _any?
self.each do |element|
return true if yield element
end
return false
end
end
a = [2, 4, 6]
a._all? { |e| e%2 == 0 } # => true
a._any? { |e| e > 4 } # =? true
(Tak naprawdę metody all?
i any?
należą do modułu Enumerable
, ale tutaj chodzi o przykład, a nie o stuprocentową poprawność.)