Ruby Closures Explained.

Ruby Closures explained. Zdjęcie Denise Johnson

jeśli nie zrozumiałeś pojęcia „zamknięcia” z innych języków, mogło to być dla Ciebie mylące początkowo w Rubim. „Zamknięcie” jest anachroniczne, z wyjątkiem Rubiego (i niektórych funkcjonalnych języków programowania).

oto Merriam-Webster definicja zamknięcia:

closure noun clo * sure \ ˈklō-zhər \ archaic: means of enclosing: enclosure

tutaj jest to samo: zamknięcie rubinowe „zamyka kilka kodów do wykonania”. To „obudowa”. Według Wikipedii zamknięcie jest techniką implementacji leksykalnego wiązania nazw w języku o pierwszorzędnych funkcjach.

rozważ ten fragment:

2.times do topic = 'Ruby closures' puts "An adventure with #{topic}."end#=> An adventure with Ruby closures.#=> An adventure with Ruby closures.#=> 2

mamy „zamknięte” topic i puts między do i end. Wszystko poza tym regionem nie może mieć dostępu do tematu, ale metoda puts może, ponieważ znajduje się w tym samym środowisku (o zasięgu leksykalnym), co przypisanie zmiennej (powiązanie nazwy). Jest to technika łączenia metod i zmiennych, którą nazywamy zamknięciem. Uwaga: { ... } i do ... end są równoważne po przekazaniu do metody. Będę ich używał zamiennie.

a może ten kawałek kodu?

lam = lambda do |topic| puts "An adventure with #{topic}."endlam.call('Ruby closures')#=> An adventure with Ruby closures

jeśli przyjrzysz się uważnie, robi to podobnie jak fragment wcześniej. A co z ostatnim?

prok = Proc.new do |topic| puts "An adventure with #{topic}."endprok.call('Ruby closures')#=> An adventure with Ruby closures.

formularze te reprezentują sposoby zamykania kodu w Ruby. W poprzednich fragmentach kodu widzieliśmy blok, lambda i Proc reprezentowane odpowiednio jako: {}, lambda {} i Proc.new {}. To są zamknięcia w Ruby. We wszystkich trzech, ustawiliśmy zmienną lokalną topici przekazaliśmy ją do lokalnej metody puts. Możesz się zastanawiać, gdzie ustawiamy zmienną topic w lami prok. Nie ustawiliśmy go jawnie jako topic = "Ruby closures", jak to zrobiliśmy w pierwszym fragmencie kodu. Powodem jest; metoda #call dla Proc i lambda robi to za nas. Jeśli spojrzysz na dokumentację dla Proc#call, mamy:

wywołanie (params, …) → objinvokuje blok, ustawiając parametry bloku na wartości w params.

jedna subtelna różnica, na którą warto zwrócić uwagę w przypadku bloków Ruby (jako zamknięć) jest taka, że w Rubim bloki nie mogą być reprezentowane jako obiekty, po prostu zapewniają prosty sposób grupowania kodu, ale nie można ich powiązać ze zmiennymi. Same w sobie są zupełnie bezużyteczne. Proc i Lambda, z drugiej strony, są, jak niektórzy nazywają obywateli pierwszej klasy.

bloki.

mam do poprawienia, ale jak wspomniałem wcześniej, same bloki nic nie znaczą. Są one zazwyczaj przekazywane jako argumenty do metod, ale pomagają nam również grupować nasz kod, aby zmodyfikować jego zachowanie. Oto przykład:

(1..10).each { |v| puts v if v.even? }# Output: ## 2# 4# 6# 8# 10

łatwo tu zauważyć, że iterujemy zakres i drukujemy wszystkie liczby parzyste. A jeśli chcemy wydrukować wszystkie liczby parzyste i pierwsze jednocześnie? Tutaj klocki pokazują swoją użyteczność. Musimy tylko zmodyfikować kod w bloku, aby to zrobić, aby uzyskać:

require 'prime'(1..20).each { |v| puts v if v.even? && v.prime? }# => 2

po prostu zmieniliśmy zachowanie naszego kodu grupując to, co jest w naszym bloku. Musieliśmy wymagać standardowej biblioteki Prime, aby użyć metody prime?. Moglibyśmy również zaimplementować naszą metodę zbierania liczb pierwszych, gdybyśmy chcieli. Widzieliśmy, jak możemy używać bloków Ruby do grupowania naszego kodu do wyliczania. Ale w tym poście mówimy o zamknięciach. Czy widziałeś już, jak powyższy blok działa jako zamknięcie, przypisując każdą liczbę w zakresie od 1 do 20 do zmiennej v? Jeśli nie, to może ten kawałek kodu tutaj:

def shout(word, position) position.times { |x| puts "#{ x }: #{ word }"}endshout("SHOUTING!", 2)# Output: ## 0: SHOUTING!# 1: SHOUTING!

w tym, przypisujemy zmienną word do łańcucha SHOUTING!, podczas gdy przypisujemy drugi parametr, liczbę całkowitą, do zmiennej position i używamy x, aby reprezentować każdą liczbę od 0 do position, ile razy chcemy wydrukować łańcuch. Pamiętaj o definicji zamknięcia. Czy ostatni kod go spełnia?

teraz wiesz trochę o blokach, ale w przedstawionych do tej pory przykładach używamy tylko bloków Ruby do hermetyzacji zachowań i przekazywania ich do metod Rubiego. Możemy również pisać nasze metody, które akceptują bloki.

w Rubim jesteśmy błogosławieni metodami yield i block_given?. Te metody pomagają nam tworzyć nasze metody, które akceptują również bloki. Za każdym razem, gdy zobaczysz yield w Rubim, Przetłumacz to jako „Hej, rób, co mówi przekazany blok”. block_given? jest dość intuicyjny. Ta metoda po prostu zwraca wartość logiczną tego, czy został przekazany blok. Oto prosty przykład, aby zrozumieć tę koncepcję:

def shout yieldend# Call the #shout method with a block.shout { puts "SHOUTING!" }# => SHOUTING!

spróbuj powyższej metody i zadzwoń yield dwa lub trzy razy i zobacz, co się stanie. Czy to ma jakiś sens? Jeśli nie przekazałeś bloku do tej metody podczas jej wywołania, zobaczysz

brak podanego bloku (yield) (LocalJumpError)

możesz spróbować. I tu przydaje się block_given?. Oto jak można skorzystać z metody block_given? :

def shout block_given? ? yield : puts("Sir, you didn't pass any block.")end# Call the #shout method without a block.shout# => Sir, you didn't pass any block.

to wszystko! Ale być może zastanawiasz się, jaki jest praktyczny użytek z tego wszystkiego: Inicjalizacja ładnego obiektu! O tak.:

module Genus class Species attr_accessor :name, :height, :weight, :ethnicity def initialize yield self if block_given? end endendhuman = Genus::Species.new do |characteristics| characteristics.name = "Emmanuel Hayford" characteristics.height = 188 # cm characteristics.weight = 104 # kg characteristics.ethnicity = "black"endp human.ethnicity# => "black"

ustawiamy self(który jest instancjowanym obiektem klasy Species) do zmiennej characteristics.

mimo że wszystko w Rubim jest obiektem, konstrukcje takie jak bloki i metody nie są. Mogą jednak zamienić je w obiekty za pomocą proc. Tak więc głównie proc są blokami reprezentowanymi jako obiekty, na których można wywoływać metody lub przydzielać je do pamięci.

przeanalizujmy nasz pierwszy przykład użycia Proc.

więc będziemy mieli ten kawałek kodu:

2.times do topic = 'Ruby closures' puts "An adventure with #{topic}."end

zmieniony na ten:

prok = Proc.new { topic = 'Ruby closures' puts "An adventure with #{topic}."}# `Proc.new` is equivalent to `proc`2.times do prok.callend# => An adventure with Ruby closures.# => An adventure with Ruby closures.

ponieważ proc jest teraz obiektem, możesz wywołać znajdującą się na nim metodę call, aby zwrócić blok.Możemy jeszcze bardziej refaktorować, aby uczynić to bardziej dynamicznym, sprawiając, że obiekt block lub Proc przyjmuje argument:

prok = Proc.new { |topic| puts "An adventure with #{topic}."}2.times do prok.call('Ruby closures')end# => An adventure with Ruby closures.# => An adventure with Ruby closures.

oto coś ciekawego; dla obiektu Proc, który przyjmuje na przykład pojedynczy argument, można wywołać go na kilka różnych sposobów. Ruby pozwala nam pominąć wywołanie call. Tak więc linia 6 powyższego kodu jest równoważna następującej:

prok.('Ruby closures') i prok === 'Ruby closures'

jeśli jednak chcesz pracować z wieloma argumentami, możesz przekazać swoje argumenty do obiektu Proc jako elementy tablicy. Załóżmy, że to nasz kod.:

prok = Proc.new { |x, y| puts x + y }

wszystkie poniższe metody wywołania prok są poprawne:

prok.call prok === prok# => 10# => 10# => 10

wszystkie powyższe są ważne dla lambda s.

Lambda.

lambda są obiektami Proc, które szanują arity i traktują słowo kluczowe return nieco inaczej.

oto zachowanie, które warto zwrócić uwagę na proc i Lambdy. Rozważ następujące kwestie:

cool_lambda = lambda { |first_name, nationality| puts "#{first_name} is #{nationality}."}cool_proc = proc { |first_name, nationality| puts "#{first_name} is #{nationality}."}cool_lambda.call("Matz", "Japanese")cool_proc.call("Aaron", "American")

Wszystko działa zgodnie z oczekiwaniami. cool_lambdapodpis przyjmuje dwa argumenty i to jest ścisłe, jeśli podasz jeden więcej lub jeden mniej argumentów, wybuchnie to czymś w rodzaju:

błędna liczba argumentów (podane 3, oczekiwane 2) (ArgumentError)

cool_proc z drugiej strony po prostu zrobi to, co określa blok i po cichu zignoruje dodatkowe paramy lub ich brak.

oto sprawa ze słowem kluczowym return z zamknięciami:

class ClosureExperiment def call_closure(closure) puts "Calling #{ check_closure_type(closure) }." closure.call puts "#{ check_closure_type(closure) } got called!" end def check_closure_type(unknown_closure) unknown_closure.lambda? ? "Lambda" : "Proc" endendclosure = ClosureExperiment.new.call_closure(lambda { return })# => Calling Lambda.# => Lambda got called!closure = ClosureExperiment.new.call_closure(proc { return })# => Calling Proc.

z powyższego można zauważyć, że podczas gdy Procs natychmiast się wyskakuje po obejrzeniu return, Lambda waha się, aż wszystko w ich kontekście zostanie wykonane.

jeszcze jedna różnica między tymi dwoma zamknięciami polega na tym, że gdy Lambda jest zdefiniowana z domyślnym argumentem i przekazana tablicy, zawsze zwraca domyślny argument przekazany do niej, podczas gdy Proc ignoruje go, chyba że nie ma żadnych dodatkowych argumentów, wtedy domyślny argument jest zaimplementowany jak w poniższym przykładzie:

lam = lambda { |a, b = :foo| p a, b }.call()# => , :foo]prok = proc { |a, b = :foo| p a, b }.call()# => prok = proc { |a, b = :foo| p a, b }.call()# => 

to by było na tyle, jak Procs i Lambda się różnią.

podsumowując, lubię myśleć o Procach jako blokach reprezentowanych jako obiekty, podczas gdy lambda są odmianą Proców, które wywołują błędy, gdy podasz niewłaściwą liczbę argumentów.

Śledź mnie na Twitterze, aby poznać Ruby, JavaScript i technologie internetowe. Rozważ zapisanie się do mojego newslettera lub zakup mi kawy, jeśli uważasz, że którekolwiek z moich treści jest pomocne.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.