Usare "return" in un blocco Ruby


87

Sto cercando di utilizzare Ruby 1.9.1 per un linguaggio di scripting incorporato, in modo che il codice "utente finale" venga scritto in un blocco Ruby. Un problema con questo è che vorrei che gli utenti fossero in grado di utilizzare la parola chiave "return" nei blocchi, quindi non devono preoccuparsi dei valori di ritorno impliciti. Con questo in mente, questo è il tipo di cose che vorrei essere in grado di fare:

def thing(*args, &block)
  value = block.call
  puts "value=#{value}"
end

thing {
  return 6 * 7
}

Se utilizzo "return" nell'esempio precedente, ottengo un'eccezione LocalJumpError. Sono consapevole del fatto che il blocco in questione è un Proc e non un lambda. Il codice funziona se rimuovo "return", ma preferirei davvero essere in grado di utilizzare "return" in questo scenario. È possibile? Ho provato a convertire il blocco in un lambda, ma il risultato è lo stesso.


perché vuoi evitare un valore di ritorno implicito?
marcgg

@marcgg - Ho una domanda correlata qui - stackoverflow.com/questions/25953519/… .
Sid Smith

Risposte:


171

Usa semplicemente nextin questo contesto:

$ irb
irb(main):001:0> def thing(*args, &block)
irb(main):002:1>   value = block.call
irb(main):003:1>   puts "value=#{value}"
irb(main):004:1> end
=> nil
irb(main):005:0>
irb(main):006:0* thing {
irb(main):007:1*   return 6 * 7
irb(main):008:1> }
LocalJumpError: unexpected return
        from (irb):7:in `block in irb_binding'
        from (irb):2:in `call'
        from (irb):2:in `thing'
        from (irb):6
        from /home/mirko/.rvm/rubies/ruby-1.9.1-p378/bin/irb:15:in `<main>'
irb(main):009:0> thing { break 6 * 7 }
=> 42
irb(main):011:0> thing { next 6 * 7 }
value=42
=> nil
  • return ritorna sempre dal metodo, ma se provi questo frammento in irb non hai il metodo, ecco perché lo hai LocalJumpError
  • breakrestituisce il valore dal blocco e termina la sua chiamata. Se il tuo blocco è stato chiamato da yieldo .call, allora si breakinterrompe anche questo iteratore
  • nextrestituisce il valore dal blocco e termina la sua chiamata. Se il tuo blocco è stato chiamato da yieldo .call, nextrestituisce il valore alla riga in cui è yieldstato chiamato

4
l'interruzione di un proc solleverà un'eccezione
gfreezy

puoi citare dove ottieni queste informazioni da quel "prossimo valore restituisce dal blocco e termina la chiamata". Voglio saperne di più.
user566245

Era dal libro The Ruby Programming Language (non ce l'ho a portata di mano adesso) se ricordo bene. Ho appena controllato google e credo che provenga da quel libro: librairie.immateriel.fr/fr/read_book/9780596516178/… e 2 next pagex da lì (non è il mio contenuto e le mie pagine, l'ho semplicemente cercato su Google). Ma consiglio vivamente il libro originale, ha molte più gemme spiegate.
MBO

Inoltre ho risposto dalla mia testa, controllando solo le cose in irb, ecco perché la mia risposta non è tecnica o completa. Per maggiori informazioni consultare il libro The Ruby Programming Language.
MBO

Vorrei che questa risposta fosse in cima. Non posso votarlo abbastanza.
btx9000

20

Non puoi farlo in Ruby.

La returnparola chiave restituisce sempre dal metodo o lambda nel contesto corrente. Nei blocchi, tornerà dal metodo in cui è stata definita la chiusura . Non è possibile eseguire la restituzione dal metodo chiamante o da lambda.

Il Rubyspec dimostra che questo è effettivamente il comportamento corretto per Ruby (certamente non una vera implementazione, ma mira alla piena compatibilità con C Ruby):

describe "The return keyword" do
# ...
describe "within a block" do
# ...
it "causes the method that lexically encloses the block to return" do
# ...
it "returns from the lexically enclosing method even in case of chained calls" do
# ...

C'è un articolo dettagliato sul ritorno da un blocco / proc qui
ComDubh

3

Lo stai guardando dal punto di vista sbagliato. Questo è un problema di thing, non di lambda.

def thing(*args, &block)
  block.call.tap do |value|
    puts "value=#{value}"
  end
end

thing {
  6 * 7
}

1

Dove viene invocata la cosa? Sei in una classe?

Potresti considerare di usare qualcosa di simile:

class MyThing
  def ret b
    @retval = b
  end

  def thing(*args, &block)
    implicit = block.call
    value = @retval || implicit
    puts "value=#{value}"
  end

  def example1
    thing do
      ret 5 * 6
      4
    end
  end

  def example2
    thing do
      5 * 6
    end
  end
end

1

Ho avuto lo stesso problema scrivendo un DSL per un framework web in ruby ​​... (il framework web Anorexic sarà eccezionale!) ...

comunque, ho scavato negli interni di ruby ​​e ho trovato una soluzione semplice usando LocalJumpError restituito quando un Proc chiama return ... funziona bene nei test fino ad ora, ma non sono sicuro che sia a prova completa:

def thing(*args, &block)
  if block
    block_response = nil
    begin
      block_response = block.call
    rescue Exception => e
       if e.message == "unexpected return"
          block_response = e.exit_value
       else
          raise e 
       end
    end
    puts "value=#{block_response}"
  else
    puts "no block given"
  end
end

l'istruzione if nel segmento di salvataggio potrebbe probabilmente assomigliare a questo:

if e.is_a? LocalJumpError

ma è un territorio inesplorato per me, quindi mi atterrò a ciò che ho testato finora.


1

Credo che questa sia la risposta corretta, nonostante gli inconvenienti:

def return_wrap(&block)
  Thread.new { return yield }.join
rescue LocalJumpError => ex
  ex.exit_value
end

def thing(*args, &block)
  value = return_wrap(&block)
  puts "value=#{value}"
end

thing {
  return 6 * 7
}

Questo hack consente agli utenti di utilizzare il ritorno nei loro processi senza conseguenze, il sé è preservato, ecc.

Il vantaggio di usare Thread qui è che in alcuni casi non otterrai LocalJumpError e il ritorno avverrà nel posto più inaspettato (oltre a un metodo di primo livello, saltando inaspettatamente il resto del suo corpo).

Lo svantaggio principale è il potenziale overhead (puoi sostituire Thread + join con solo yieldse è sufficiente nel tuo scenario).


1

Ho trovato un modo, ma si tratta di definire un metodo come passaggio intermedio:

def thing(*args, &block)
  define_method(:__thing, &block)
  puts "value=#{__thing}"
end

thing { return 6 * 7 }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.