Ruby/Domknięcia i obiekty procedurowe

Domknięcia i obiekty procedurowe

edytuj

Domknięcia

edytuj

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

Obiekty procedurowe

edytuj

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.

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

edytuj

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