Usando 'retorno' en un bloque Ruby

87

Estoy tratando de usar Ruby 1.9.1 para un lenguaje de scripting incrustado, de modo que el código de "usuario final" se escriba en un bloque Ruby. Un problema con esto es que me gustaría que los usuarios pudieran usar la palabra clave 'return' en los bloques, para que no tengan que preocuparse por los valores de retorno implícitos. Con esto en mente, este es el tipo de cosas que me gustaría poder hacer:

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

thing {
  return 6 * 7
}

Si uso 'return' en el ejemplo anterior, obtengo un LocalJumpError. Soy consciente de que esto se debe a que el bloque en cuestión es un Proc y no un lambda. El código funciona si elimino 'return', pero realmente preferiría poder usar 'return' en este escenario. es posible? Intenté convertir el bloque a lambda, pero el resultado es el mismo.

MetaFu
fuente
¿Por qué quiere evitar un valor de retorno implícito?
marcgg
@marcgg - Tengo una pregunta relacionada aquí - stackoverflow.com/questions/25953519/… .
Sid Smith

Respuestas:

171

Simplemente use nexten este contexto:

$ 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 siempre regresa del método, pero si prueba este fragmento en irb no tiene método, es por eso que tiene LocalJumpError
  • breakdevuelve el valor del bloque y finaliza su llamada. Si su bloque fue llamado por yieldo .call, entonces también se breakrompe de este iterador
  • nextdevuelve el valor del bloque y finaliza su llamada. Si su bloque fue llamado por yieldo .call, entonces nextdevuelve el valor a la línea donde yieldfue llamado
MBO
fuente
4
romper un proceso generará una excepción
gfreezy
¿Puede citar de dónde obtiene esta información de ese "siguiente valor devuelve el bloque y finaliza la llamada". Quiero leer más sobre eso.
user566245
Era del libro The Ruby Programming Language (no lo tengo a mano en este momento) si no recuerdo mal. Acabo de revisar Google y creo que es de ese libro: librairie.immateriel.fr/fr/read_book/9780596516178/… y 2 páginas siguientes desde allí (no es mi contenido y mis páginas, simplemente lo busqué en Google). Pero realmente recomiendo el libro original, tiene muchas más gemas explicadas.
MBO
También respondí desde mi cabeza, solo revisando cosas en irb, por eso mi respuesta no es técnica ni completa. Para obtener más información, consulte el libro El lenguaje de programación Ruby.
MBO
Ojalá esta respuesta estuviera en la parte superior. No puedo votarlo lo suficiente.
btx9000
20

No puedes hacer eso en Ruby.

La returnpalabra clave siempre regresa del método o lambda en el contexto actual. En bloques, volverá del método en el que se definió el cierre . No se puede hacer que regrese del método de llamada o lambda.

El Rubyspec demuestra que este es de hecho el comportamiento correcto para Ruby (es cierto que no es una implementación real, pero tiene como objetivo la compatibilidad total 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
# ...
molf
fuente
Hay un artículo detallado sobre el regreso de un bloque / proceso aquí
ComDubh
3

Lo estás mirando desde el punto de vista equivocado. Este es un problema de thinglambda, no.

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

thing {
  6 * 7
}
Simone Carletti
fuente
1

¿Dónde se invoca la cosa? ¿Estás dentro de una clase?

Puede considerar usar algo como esto:

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
giorgian
fuente
1

Tuve el mismo problema al escribir un DSL para un marco web en ruby ​​... (¡el marco web Anorexic será genial!) ...

de todos modos, busqué en las partes internas de ruby ​​y encontré una solución simple usando el LocalJumpError devuelto cuando regresan las llamadas de Proc ... funciona bien en las pruebas hasta ahora, pero no estoy seguro de que sea completamente a prueba:

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

la declaración if en el segmento de rescate probablemente podría verse así:

if e.is_a? LocalJumpError

pero es un territorio desconocido para mí, así que me ceñiré a lo que probé hasta ahora.

Myst
fuente
1

Creo que esta es la respuesta correcta, a pesar de los inconvenientes:

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
}

Este truco permite a los usuarios usar return en sus procesos sin consecuencias, se conserva el self, etc.

La ventaja de usar Thread aquí es que, en algunos casos, no obtendrá LocalJumpError, y el retorno ocurrirá en el lugar más inesperado (en un método de nivel superior, omitiendo inesperadamente el resto de su cuerpo).

La principal desventaja es la sobrecarga potencial (puede reemplazar Thread + join con solo yieldsi eso es suficiente en su escenario).

Cezary Baginski
fuente
1

Encontré una forma, pero implica definir un método como un paso intermedio:

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

thing { return 6 * 7 }
s12chung
fuente