Bloki i plony w Ruby

275

Próbuję zrozumieć bloki i yieldich działanie w Ruby.

Jak yieldstosować? Wiele aplikacji Rails, na które patrzyłem, używało yieldw dziwny sposób.

Czy ktoś może mi wyjaśnić lub pokazać, gdzie mam je zrozumieć?

Matt Elhotiby
źródło
2
Możesz być zainteresowany odpowiedzią na funkcję Ruby dotyczącą wydajności w odniesieniu do informatyki . Chociaż jest to nieco inne pytanie niż twoje, może rzucić nieco światła na tę sprawę.
Ken Bloom

Odpowiedzi:

393

Tak, na początku jest to trochę zagadkowe.

W języku Ruby metody mogą odbierać blok kodu w celu wykonania dowolnych segmentów kodu.

Gdy metoda oczekuje bloku, wywołuje go, wywołując yieldfunkcję.

Jest to bardzo przydatne, na przykład, do iteracji po liście lub zapewnienia niestandardowego algorytmu.

Weź następujący przykład:

Mam zamiar zdefiniować Personklasę zainicjowaną nazwą i podać do_with_namemetodę, która po wywołaniu przekaże nameatrybut do otrzymanego bloku.

class Person 
    def initialize( name ) 
         @name = name
    end

    def do_with_name 
        yield( @name ) 
    end
end

Pozwoliłoby nam to wywołać tę metodę i przekazać dowolny blok kodu.

Na przykład, aby wydrukować nazwę, zrobilibyśmy:

person = Person.new("Oscar")

#invoking the method passing a block
person.do_with_name do |name|
    puts "Hey, his name is #{name}"
end

Wydrukowałby:

Hey, his name is Oscar

Zauważ, że blok otrzymuje jako parametr zmienną o nazwie name(uwaga: możesz wywoływać tę zmienną w dowolny sposób, ale warto ją wywołać name). Po wywołaniu kodu yieldwypełnia ten parametr wartością @name.

yield( @name )

Możemy zapewnić kolejny blok, aby wykonać inną akcję. Na przykład odwróć nazwę:

#variable to hold the name reversed
reversed_name = ""

#invoke the method passing a different block
person.do_with_name do |name| 
    reversed_name = name.reverse
end

puts reversed_name

=> "racsO"

Zastosowaliśmy dokładnie tę samą metodę ( do_with_name) - to tylko inny blok.

Ten przykład jest trywialny. Bardziej interesujące zastosowania to filtrowanie wszystkich elementów w tablicy:

 days = ["monday", "tuesday", "wednesday", "thursday", "friday"]  

 # select those which start with 't' 
 days.select do | item |
     item.match /^t/
 end

=> ["tuesday", "thursday"]

Lub możemy również dostarczyć niestandardowy algorytm sortowania, na przykład na podstawie rozmiaru ciągu:

 days.sort do |x,y|
    x.size <=> y.size
 end

=> ["monday", "friday", "tuesday", "thursday", "wednesday"]

Mam nadzieję, że to pomoże ci lepiej to zrozumieć.

BTW, jeśli blok jest opcjonalny, powinieneś go nazwać:

yield(value) if block_given?

Jeśli nie jest opcjonalny, po prostu go wywołaj.

EDYTOWAĆ

@hmak utworzył repl.it dla tych przykładów: https://repl.it/@makstaks/blocksandyieldsrubyexample

OscarRyz
źródło
jak to drukuje, racsOjeśli the_name = ""
Paritosh Piplewar
2
Niestety, nazwa jest zmienną instancji zainicjowaną za pomocą "Oscar" (nie jest bardzo jasne w odpowiedzi)
OscarRyz
Co z takim kodem? person.do_with_name {|string| yield string, something_else }
f .ardelian
7
Tak więc, w kategoriach Javascripty, jest to znormalizowany sposób przekazywania wywołania zwrotnego do danej metody i wywoływania jej. Dziękuję za wyjaśnienie!
yitznewton,
Mówiąc bardziej ogólnie - blok to rubinowy „ulepszony” cukier składniowy dla wzorca strategii. ponieważ typowym zastosowaniem jest dostarczenie kodu do zrobienia czegoś w kontekście innej operacji. Ale ulepszenia ruby ​​otwierają drogę do tak fajnych rzeczy, jak pisanie DSL-ów za pomocą bloku do przekazywania kontekstu
Roman Bułhakow
25

W języku Ruby metody mogą sprawdzić, czy zostały wywołane w taki sposób, że oprócz zwykłych argumentów podano blok. Zwykle odbywa się to za pomocą block_given?metody, ale można również odwołać się do bloku jako jawnego Proc, poprzedzając znak ampersand ( &) przed ostateczną nazwą argumentu.

Jeśli metoda jest wywoływana z blokiem, metoda może yieldsterować blokiem (wywołać blok) z pewnymi argumentami, jeśli to konieczne. Rozważ tę przykładową metodę, która pokazuje:

def foo(x)
  puts "OK: called as foo(#{x.inspect})"
  yield("A gift from foo!") if block_given?
end

foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How nice =)

Lub używając specjalnej składni argumentu blokowego:

def bar(x, &block)
  puts "OK: called as bar(#{x.inspect})"
  block.call("A gift from bar!") if block
end

bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How nice =)
maerika
źródło
Dobrze znać różne sposoby wyzwalania bloku.
LPing
22

Całkiem możliwe, że ktoś udzieli tu naprawdę szczegółowej odpowiedzi, ale zawsze uważałem ten post Roberta Sosińskiego za doskonałe wyjaśnienie subtelności między blokami, procami i lambdami.

Powinienem dodać, że moim zdaniem post, do którego prowadzę link, jest specyficzny dla ruby ​​1.8. Niektóre rzeczy uległy zmianie w Ruby 1.9, na przykład zmienne blokowe są lokalne dla bloku. W wersji 1.8 otrzymasz coś takiego:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"

Podczas gdy 1.9 zapewni Ci:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"

Nie mam 1.9 na tym komputerze, więc powyższe może zawierać błąd.

theIV
źródło
Świetny opis w tym artykule, zajęło mi kilka miesięcy, aby dowiedzieć się, że to wszystko na własną rękę =)
maerics
Zgadzam się. Chyba nie znałem połowy tego wszystkiego, dopóki nie przeczytałem.
theIV
Zaktualizowany link to teraz także 404. Oto link Wayback Machine .
klenwell
@klenwell dzięki za zgłoszenie, zaktualizowałem link ponownie.
theIV
13

Chciałem w pewnym sensie dodać, dlaczego tak robisz w odpowiedzi na i tak świetne odpowiedzi.

Nie mam pojęcia, z jakiego języka pochodzisz, ale zakładając, że jest to język statyczny, tego rodzaju rzeczy będą wyglądały znajomo. W ten sposób czytasz plik w Javie

public class FileInput {

  public static void main(String[] args) {

    File file = new File("C:\\MyFile.txt");
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    DataInputStream dis = null;

    try {
      fis = new FileInputStream(file);

      // Here BufferedInputStream is added for fast reading.
      bis = new BufferedInputStream(fis);
      dis = new DataInputStream(bis);

      // dis.available() returns 0 if the file does not have more lines.
      while (dis.available() != 0) {

      // this statement reads the line from the file and print it to
        // the console.
        System.out.println(dis.readLine());
      }

      // dispose all the resources after using them.
      fis.close();
      bis.close();
      dis.close();

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Ignorując cały łańcuch związany z strumieniem, Chodzi o to

  1. Zainicjuj zasób, który należy oczyścić
  2. użyj zasobu
  3. upewnij się, że to posprzątasz

Tak to robisz w rubinie

File.open("readfile.rb", "r") do |infile|
    while (line = infile.gets)
        puts "#{counter}: #{line}"
        counter = counter + 1
    end
end

Zupełnie inaczej. Rozbicie tego

  1. powiedz klasie File, jak zainicjować zasób
  2. powiedz klasie plików, co z nią zrobić
  3. śmiej się z gości z Java, którzy wciąż piszą ;-)

Tutaj, zamiast zajmować się krokiem pierwszym i drugim, w zasadzie delegujesz to do innej klasy. Jak widać, radykalnie zmniejsza to ilość kodu, który musisz napisać, co ułatwia czytanie i zmniejsza ryzyko wycieków pamięci lub braku możliwości usunięcia blokad plików.

Teraz nie jest tak, że nie można zrobić czegoś podobnego w java, w rzeczywistości ludzie robią to od dziesięcioleci. To się nazywa wzór strategii . Różnica polega na tym, że bez bloków, w przypadku czegoś tak prostego jak przykład pliku, strategia staje się nadmierna z powodu ilości klas i metod, które musisz napisać. W przypadku bloków jest to tak prosty i elegancki sposób, aby to zrobić, że NIE ma sensu NIE strukturyzować kodu w ten sposób.

Nie jest to jedyny sposób, w jaki używane są bloki, ale inne (jak wzorzec Konstruktora, który można zobaczyć w form_for api w szynach) są na tyle podobne, że powinno być oczywiste, co się dzieje, gdy obejdziesz się tym. Kiedy widzisz bloki, zwykle bezpiecznie jest założyć, że wywołanie metody jest tym, co chcesz zrobić, a blok opisuje, jak chcesz to zrobić.

Matt Briggs
źródło
5
Uprośćmy to trochę: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endi śmiejmy się jeszcze mocniej z ludzi z Java.
Michael Hampton
1
@MichaelHampton, śmiej się po przeczytaniu pliku o długości kilku gigabajtów.
akostadinov
@akostadinov Nie ... to sprawia, że ​​chcę płakać!
Michael Hampton
3
@MichaelHampton Lub, jeszcze lepiej: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(plus brak problemów z pamięcią)
pozew Fund Moniki
12

Uważam, że ten artykuł jest bardzo przydatny. W szczególności następujący przykład:

#!/usr/bin/ruby

def test
  yield 5
  puts "You are in the method test"
  yield 100
end

test {|i| puts "You are in the block #{i}"}

test do |i|
    puts "You are in the block #{i}"
end

co powinno dać następujące dane wyjściowe:

You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100

Zasadniczo za każdym razem, gdy zostanie wykonane wywołanie yieldruby, uruchomi kod w dobloku lub w środku {}. Jeśli podano parametr, yieldto zostanie on podany jako parametr do dobloku.

Dla mnie to był pierwszy raz, kiedy naprawdę zrozumiałem, co dorobią bloki. Zasadniczo jest to sposób, w jaki funkcja umożliwia dostęp do wewnętrznych struktur danych, zarówno w celu iteracji, jak i konfiguracji funkcji.

Kiedy więc w szynach piszesz:

respond_to do |format|
  format.html { render template: "my/view", layout: 'my_layout' }
end

Uruchomi to respond_tofunkcję, która zwróci doblok z parametrem (wewnętrznym) format. Następnie wywołujesz .htmlfunkcję dla tej zmiennej wewnętrznej, co z kolei daje blok kodu do uruchomienia renderpolecenia. Pamiętaj, że .htmlprzyniesie to tylko wtedy, gdy jest to wymagany format pliku. (technicznie: te funkcje faktycznie block.callnie używają tego, yieldco widać ze źródła, ale funkcjonalność jest zasadniczo taka sama, zobacz to pytanie w celu omówienia.) Zapewnia to sposób, aby funkcja wykonała pewną inicjalizację, a następnie pobierała dane wejściowe z kodu wywołującego i następnie w razie potrzeby kontynuować przetwarzanie.

Innymi słowy, jest podobny do funkcji przyjmującej anonimową funkcję jako argument, a następnie wywołującej ją w javascript.

zelanix
źródło
8

W Ruby blok jest zasadniczo fragmentem kodu, który można przekazać i wykonać dowolną metodą. Bloki są zawsze używane z metodami, które zwykle przekazują im dane (jako argumenty).

Bloki są szeroko stosowane w klejnotach Ruby (w tym Railsach) oraz w dobrze napisanym kodzie Ruby. Nie są obiektami, dlatego nie można ich przypisywać do zmiennych.

Podstawowa składnia

Blok to fragment kodu ujęty w {} lub do..end. Zgodnie z konwencją w przypadku bloków jednowierszowych należy stosować składnię nawiasów klamrowych, aw przypadku bloków wieloliniowych należy stosować składnię do..end.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Każda metoda może otrzymać blok jako domyślny argument. Blok jest wykonywany przez instrukcję return w ramach metody. Podstawowa składnia to:

def meditate
  print "Today we will practice zazen"
  yield # This indicates the method is expecting a block
end 

# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }

Output:
Today we will practice zazen for 40 minutes.

Po osiągnięciu instrukcji fedrowania metoda medytacji daje kontrolę nad blokiem, kod w bloku jest wykonywany, a kontrola jest zwracana do metody, która wznawia wykonywanie natychmiast po instrukcji fed.

Gdy metoda zawiera instrukcję dochodu, oczekuje otrzymania bloku w czasie wywołania. Jeśli blok nie zostanie podany, wyjątek zostanie zgłoszony po osiągnięciu instrukcji dochodu. Możemy uczynić blok opcjonalnym i uniknąć generowania wyjątku:

def meditate
  puts "Today we will practice zazen."
  yield if block_given? 
end meditate

Output:
Today we will practice zazen. 

Nie można przekazać wielu bloków do metody. Każda metoda może otrzymać tylko jeden blok.

Zobacz więcej na: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


źródło
To jest (jedyna) odpowiedź, która naprawdę pozwala mi zrozumieć, co jest blokiem i wydajnością oraz jak z nich korzystać.
Eric Wang
5

Czasami używam „wydajności” w ten sposób:

def add_to_http
   "http://#{yield}"
end

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
Samet Sazak
źródło
Dobrze ale dlaczego ? Istnieje wiele powodów, takich jak to, Loggerże użytkownik nie musi wykonywać jakiegoś zadania, jeśli użytkownik nie musi tego robić. Powinieneś jednak wyjaśnić swoje ...
Ulysse BN
4

Mówiąc prościej, Yields pozwala, aby tworzona metoda przyjmowała i wywoływała bloki. Słowo kluczowe wydajności w szczególności jest miejscem, w którym zostaną wykonane „rzeczy” w bloku.

ntarpey
źródło
1

Chciałbym tu wspomnieć o dwóch kwestiach dotyczących wydajności. Po pierwsze, podczas gdy wiele odpowiedzi tutaj mówi o różnych sposobach przekazania bloku do metody wykorzystującej wydajność, porozmawiajmy również o przepływie sterowania. Jest to szczególnie istotne, ponieważ możesz dać WIELU CZASÓW blokowi. Spójrzmy na przykład:

class Fruit
  attr_accessor :kinds

  def initialize 
    @kinds = %w(orange apple pear banana)
  end

  def each 
    puts 'inside each'
    3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
  end  
end

f = Fruit.new
f.each do |kind|
  puts 'inside block'
end    

=> inside each
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block

Po wywołaniu każdej metody wykonuje się linia po linii. Teraz, gdy przejdziemy do bloku 3. razy, ten blok zostanie wywołany 3 razy. Za każdym razem, gdy wywołuje plon. Ta wydajność jest powiązana z blokiem związanym z metodą, która wywołała każdą metodę. Należy zauważyć, że za każdym razem, gdy wywoływana jest wydajność, zwraca kontrolę z powrotem do bloku każdej metody w kodzie klienta. Po zakończeniu wykonywania blok wraca do bloku 3. razy. I dzieje się to 3 razy. Tak więc blok w kodzie klienta jest wywoływany 3 odrębnymi okazjami, ponieważ fedrunek jest jawnie nazywany 3 oddzielnymi czasami.

Moja druga uwaga dotyczy enum_for i fed. enum_for tworzy instancję klasy Enumerator, a ten obiekt Enumerator odpowiada również na fed.

class Fruit
  def initialize
    @kinds = %w(orange apple)
  end

  def kinds
    yield @kinds.shift
    yield @kinds.shift
  end
end

f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
 => "orange" 
enum.next
 => "apple" 

Zauważ więc, że za każdym razem, gdy wywołujemy rodzaje za pomocą zewnętrznego iteratora, wywoła to wydajność tylko raz. Następnym razem, gdy go nazwiemy, wywoła następną wydajność i tak dalej.

Jest ciekawa ciekawostka w odniesieniu do enum_for. Dokumentacja online zawiera następujące informacje:

enum_for(method = :each, *args)  enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.

str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }    
# => 120
# => 121
# => 122

Jeśli nie podasz symbolu jako argumentu dla enum_for, ruby ​​podłączy moduł wyliczający do każdej metody odbiornika. Niektóre klasy nie mają każdej metody, na przykład klasa String.

str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String

Tak więc, w przypadku niektórych obiektów wywoływanych za pomocą enum_for, musisz wyraźnie określić, jaka będzie twoja metoda wyliczania.

Donato
źródło
0

Wydajność może być użyta jako blok bezimienny do zwrócenia wartości w metodzie. Rozważ następujący kod:

Def Up(anarg)
  yield(anarg)
end

Możesz utworzyć metodę „W górę”, która ma przypisany jeden argument. Możesz teraz przypisać ten argument, aby uzyskać wynik, który wywoła i wykona powiązany blok. Możesz przypisać blok po liście parametrów.

Up("Here is a string"){|x| x.reverse!; puts(x)}

Gdy wywołanie metody Up daje argument z argumentem, jest on przekazywany do zmiennej blokowej w celu przetworzenia żądania.

gkstr1
źródło