2015-08-13 8 views
5

Zastanawiam się, jak przekazać blok do metody, która uczyni metodę return na yield.Zrozumienie powracania z proców w Rubim

Naiwny aproach nie działa:

def run(&block) 
    block.call 
end 

run { return :foo } # => LocalJumpError 

owijania w innym proc ma ten sam efekt:

def run(&block) 
    proc { block.call }.call 
end 

run { return :bar } # => LocalJumpError 

więc pomyślałem, że oświadczenie return jest związany z receiver prądu binding. Jednakże, próbując go z instance_eval okazał mi źle:

class ProcTest 
    def run(&block) 
    puts "run: #{[binding.local_variables, binding.receiver]}" 
    instance_eval(&block) 
    end 
end 

pt = ProcTest.new 
binding_inspector = proc { puts "proc: #{[binding.local_variables, binding.receiver]}" } 
puts "main: #{[binding.local_variables, binding.receiver]}" 
    # => main: [[:pt, :binding_inspector], main] 
binding_inspector.call 
    # => proc: [[:pt, :binding_inspector], main] 
pt.run(&binding_inspector) 
    # => run: [[:block], #<ProcTest:0x007f4987b06508>] 
    # => proc: [[:pt, :binding_inspector], #<ProcTest:0x007f4987b06508>] 
pt.run { return :baz } 
    # => run: [[:block], #<ProcTest:0x007f4987b06508>] 
    # => LocalJumpError 

więc pytania są:

  1. Jak można to zrobić?
  2. W jaki sposób kontekst powrotu jest powiązany z instrukcją return. Czy to połączenie jest dostępne za pośrednictwem interfejsu API języka?
  3. Czy zostało to celowo wdrożone w taki sposób? Jeśli tak, dlaczego? Jeśli nie - jakie są przeszkody, aby to naprawić?

Odpowiedz

1

return w bloku zwraca z otaczającej metody, gdy blok jest zdefiniowany (tj. Zamknięcie, w którym blok jest tworzony). W twoim przykładzie nie ma zamkniętego bloku, z którego można by powrócić, stąd twój wyjątek.

to łatwo wykazać:

def foo(&block) 
    puts yield 
    puts "we won't get here" 
end 

def bar 
    foo { return "hi from the block"; puts "we never get here" } 
    puts "we never get here either" 
end 

puts bar # => "hi from the block" (only printed once; the puts in `foo` is not executed) 

powrotny w proc natychmiast powrócić z proc, a nie z tym sposobem na stosie pod proc:

def foo(&block) 
    puts yield 
    puts "we will get here" 
end 

def bar 
    foo &->{ return "hi from the proc"; puts "we never get here" } 
    puts "we will get here too" 
end 

puts bar 
# hi from the proc  # puts from foo 
# we will get here  # puts from foo 
# we will get here too # puts from bar 

powodu tych zachowań, nie ma sposobu osiągnięcia pożądanego zachowania, w którym return w danym bloku będzie wykonywał return w metodzie, z której blok jest wywoływany, chyba że blok został zdefiniowany w tym zakresie, ponieważ wymagałoby to jednego z istniejące zachowania nie działają.

Można osiągnąć coś takiego z rzutów ... CATCH, która jest trochę-jakoś użyteczny jako sposób na zip stos z dowolnej głębokości, ale nie można powrócić dowolne wartości z nim:

def foo(&block) 
    yield 
    puts "we won't get here" 
end 

catch(:escape) do 
    foo &->{ throw :escape } 
end 
+0

Nie odbierzesz jedno pytanie. Pierwszy punkt - Wiem, że otrzymuję wyjątek, ponieważ próbuję "zwrócić" z zakresu * głównego * i że 'return' jest związany z zakresem, który go zdefiniował. O drugiej części - wiem * lambdas * mają różne semantyki 'return' z regularnych * procs *, pytanie nie dotyczyło ich. Wiem też o 'throw' -' catch', chodziło o zrozumienie 'return'. To tak, jakby zapytać * dlaczego trawa jest zielona? * I uzyskać odpowiedź * patrząc na tę trawę, widać, że jest zielona, ​​woda nie jest zielona i można pomalować sztuczną trawę w kolorze innym niż zielony *. – ndn

+0

Nie ma możliwości, aby Ruby przekazała blok do metody, która może powrócić z bloku poza metodę, do której został przekazany blok, ponieważ takie zachowanie powrotne wykluczałoby się wzajemnie z istniejącymi zwrotami. Nie wiem * dlaczego * został zaprojektowany w ten sposób, ale odpowiedź "dlaczego Ruby tego nie ma", a tym samym odpowiedź na pytanie Q1 (nie możesz), Q3 jest zatem nieistotny (nie jest to błąd być ustalonym, to celowy wybór wzajemnie wykluczających się alternatyw), Q2 będzie wymagało uzyskania odpowiedzi na parser i kod VM, ale odpowiedź brzmi: "kontekst nie jest odsłonięty". –

+0

Co rozumiesz przez *, ponieważ takie zachowanie powrotne wykluczałoby się wzajemnie z istniejącymi zwrotnymi zachowaniami * i * celowym wyborem wzajemnie wykluczających się alternatyw *? Jakie pożądane zachowanie zostałoby złamane, gdyby zostało zaimplementowane, aby powrócić z metody wywołującej. Czy wykonanie procesu nie jest dodawane do stosu, gdy jest wywoływane? * dlaczego Ruby nie ma tego * oznacza, że ​​jest to dodatkowa funkcja, która nie jest. – ndn

2

Myślałem, że oświadczenie return jest związany z receiver bieżącego binding.

Tylko metody mają odbiornik.return nie jest metoda:

defined? return #=> "expression" 

Próbuje wywołać go jako metoda nie działa:

def foo 
    send(:return, 123) 
end 

foo #=> undefined method `return' 

próbuje ją z instance_eval okazał mi źle

Choć instance_eval ocenia blok w kontekście odbiornika (dzięki czemu masz dostęp do metod instancji odbiorników i zmiennych instancji):

class MyClass 
    def foo(&block) 
    @var = 123 
    instance_eval(&block) 
    end 
end 

MyClass.new.foo { instance_variables } 
#=> [:@var] 

... robi nie ocenić bloku w bieżącym wiązania (więc nie ma dostępu do żadnych zmiennych lokalnych):

class MyClass 
    def foo(&block) 
    var = 123 
    instance_eval(&block) 
    end 
end 

MyClass.new.foo { local_variables } 
#=> [] 

Jak można to zrobić ?

Można użyć eval, ale to wymaga ciąg:

def foo 
    var = 123 
    eval yield 
    nil 
end 

foo { "return var * 2" } 
#=> 246 

lub przepuszczanie wiązanie do bloku (znowu przy użyciu eval):

def foo 
    var = 123 
    yield binding 
    nil 
end 

foo { |b| b.eval "return var * 2" } 
#=> 246 
+1

eval jest prawdopodobnie najlepszym rozwiązaniem, ponieważ kod dostaje eval'd w zakres metody plonowania, ale "wydajność ewaluacyjna" wydaje się niezwykle niebezpieczna; 'binding yield' jest prawdopodobnie najlepszym stosunkiem. Należy pamiętać, że ten kod zostanie przeanalizowany, skompilowany do iseq, a następnie wykonywany przy każdym wywołaniu, które może być mniejsze niż gwiezdna wydajność WRT - umieszczam go na poziomie około 100k IPS - o rząd wielkości wolniej niż operacja blokowa bez udziału eval. –

+1

@ChrisHeald próbuje wywołać 'return' z zewnątrz metody prawdopodobnie nie jest najlepszym pomysłem ;-) – Stefan

+0

Myślałem, że * proc * wywołanie powinno po prostu dodać ciało * proc * do bieżącego stosu i powrócić z że * proc * powinien zwrócić stos do ostatniej rzeczy na stosie (inaczej metoda, która go nazwała). Ponieważ tak nie jest, myślałem, że miejsce, z którego zostanie zwolniony "powrót", powinno być gdzieś przechowywane. 'block.binding.receiver' wydawało się logiczne. I chociaż 'instance_eval' nie zastępuje całkowicie wiązania, zmienia je (jak widać -" odbiornik "jest inny). – ndn