Usando 'return' em um bloco Ruby

86

Estou tentando usar Ruby 1.9.1 para uma linguagem de script incorporada, de modo que o código do "usuário final" seja escrito em um bloco Ruby. Um problema com isso é que eu gostaria que os usuários pudessem usar a palavra-chave 'return' nos blocos, para que não precisassem se preocupar com valores de retorno implícitos. Com isso em mente, este é o tipo de coisa que eu gostaria de ser capaz de fazer:

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

thing {
  return 6 * 7
}

Se eu usar 'return' no exemplo acima, obtenho um LocalJumpError. Estou ciente de que isso ocorre porque o bloco em questão é um Proc e não um lambda. O código funcionará se eu remover 'return', mas eu realmente prefiro poder usar 'return' neste cenário. Isso é possível? Tentei converter o bloco em lambda, mas o resultado é o mesmo.

MetaFu
fonte
por que você deseja evitar um valor de retorno implícito?
marcgg
@marcgg - Tenho uma pergunta relacionada aqui - stackoverflow.com/questions/25953519/… .
Sid Smith de

Respostas:

170

Basta usar nextneste 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 sempre retorna do método, mas se você testar este trecho no irb você não tem o método, é por isso que você tem LocalJumpError
  • breakretorna o valor do bloco e termina sua chamada. Se o seu bloco foi chamado por yieldou .call, então breakinterrompe este iterador também
  • nextretorna o valor do bloco e termina sua chamada. Se o seu bloco foi chamado por yieldou .call, então nextretorna o valor para a linha onde yieldfoi chamado
MBO
fonte
4
interromper uma proc irá gerar uma exceção
gfreezy
você pode citar onde você obtém essa informação de que "próximo retorna o valor do bloco e termina a chamada". Eu quero ler mais sobre isso.
user566245
Era do livro The Ruby Programming Language (não o tenho em mãos agora), se bem me lembro. Acabei de verificar o google e acredito que seja daquele livro: librairie.immateriel.fr/fr/read_book/9780596516178/… e 2 next pagex a partir daí (não é meu conteúdo e minhas páginas, eu apenas pesquisei). Mas eu realmente recomendo o livro original, ele tem muito mais joias explicadas.
MBO de
Também respondi da minha cabeça, apenas verificando as coisas no irb, por isso minha resposta não é técnica ou completa. Para mais informações, verifique o livro The Ruby Programming Language.
MBO de
Eu gostaria que essa resposta estivesse no topo. Eu não posso votar a favor o suficiente.
btx9000 01 de
20

Você não pode fazer isso em Ruby.

A returnpalavra-chave sempre retorna do método ou lambda no contexto atual. Em blocos, ele retornará do método em que o fechamento foi definido . Não pode ser feito para retornar do método de chamada ou lambda.

O Rubyspec demonstra que este é realmente o comportamento correto para Ruby (reconhecidamente não é uma implementação real, mas visa compatibilidade total com 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
fonte
Há um artigo detalhado sobre como retornar de um block / proc aqui
ComDubh
3

Você está olhando do ponto de vista errado. Este é um problema thing, não do lambda.

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

thing {
  6 * 7
}
Simone Carletti
fonte
1

Onde a coisa é invocada? Você está dentro de uma classe?

Você pode considerar o uso de algo assim:

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
fonte
1

Tive o mesmo problema ao escrever um DSL para um framework web em ruby ​​... (o framework web Anorexic vai arrasar!) ...

de qualquer maneira, eu pesquisei os detalhes internos do ruby ​​e encontrei uma solução simples usando o LocalJumpError retornado quando uma chamada Proc retorna ... ele funciona bem nos testes até agora, mas não tenho certeza se é à 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

a instrução if no segmento de resgate provavelmente seria algo assim:

if e.is_a? LocalJumpError

mas é um território desconhecido para mim, então vou continuar com o que testei até agora.

Myst
fonte
1

Acredito que esta seja a resposta correta, apesar das desvantagens:

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 hack permite que os usuários usem return em seus procs sem consequências, self é preservado, etc.

A vantagem de usar Thread aqui é que em alguns casos você não obterá o LocalJumpError - e o retorno acontecerá no lugar mais inesperado (ao lado de um método de nível superior, ignorando inesperadamente o resto de seu corpo).

A principal desvantagem é a sobrecarga potencial (você pode substituir o Thread + join apenas com o yieldse isso for suficiente em seu cenário).

Cezary Baginski
fonte
1

Eu encontrei uma maneira, mas envolve definir um método como uma etapa intermediária:

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

thing { return 6 * 7 }
s12chung
fonte