Как предотвратить проблемы с `return` из блока при использовании Ruby` yield`

Как только каждый программист Ruby обнаруживает, что вызовы или procs, содержащие инструкции return, могут быть опасны, поскольку это может выйти из вашего текущего контекста:

def some_method(&_block)
   puts 1
   yield
   # The following line will never be executed in this example
   # as the yield is actually a `yield-and-return`.
   puts 3
end

def test
  some_method do
    puts 2
    return
  end
end

test

# This prints "1\n2\n" instead of "1\n2\n3\n"    

В случаях, когда вы хотите быть абсолютно уверены, что некоторые из ваших кодов запускаются после вызова блока или proc, вы можете использовать конструкцию begin ... ensure. Но поскольку ensure также вызывается, если во время урока есть исключение, требуется немного больше работы.

Я создал крошечный модуль, который рассматривает эту проблему двумя разными способами:

  • Используя safe_yield, выясняется, действительно ли возвращаемый блок или proc возвращается с использованием ключевого слова return. Если это так, возникает исключение.

    unknown_block = proc do
      return
    end 
    
    ReturnSafeYield.safe_yield(unknown_block)
    # => Raises a UnexpectedReturnException exception
    
  • Используя call_then_yield, вы можете вызвать блок, а затем убедиться, что выполняется второй блок, даже если первый блок содержит оператор return.

    unknown_block = proc do
      return
    end
    ReturnSafeYield.call_then_yield(unknown_block) do
      # => This line is called even though the above block contains a `return`.
    end
    

Я собираюсь создать быстрый Gem из этого или есть встроенное решение для предотвращения быстрого возврата из вложенного блока, который я пропустил?

Ответ 1

Существует встроенное решение для определения того, содержит ли блок инструкцию return.

Вы можете использовать RubyVM::InstructionSequence.disasm, чтобы разобрать блок, переданный пользователем, затем найдите его для throw 1, который представляет собой return.

Здесь пример реализации:

def safe_yield(&block)
  if RubyVM::InstructionSequence.disasm(block) =~ /^\d+ throw +1$/
    raise LocalJumpError
  end

  block.call
end

Здесь вы можете включить его в свою библиотеку:

def library_method(&block)
  safe_yield(&block)
  puts "library_method succeeded"
rescue LocalJumpError
  puts "library_method encountered illegal return but resumed execution"
end

И здесь пользовательский опыт для хорошо себя работающего и плохого пользователя:

def nice_user_method
  library_method { 1 + 1 }
end

nice_user_method
# library_method succeeded

def naughty_user_method
  library_method { return false if rand > 0.5 }
end

naughty_user_method
# library_method encountered illegal return but resumed execution

Комментарий:

Использование raise LocalJumpError/rescue LocalJumpError охватывает проблемы, с которыми вы столкнулись при использовании обложки ensure.

Я выбрал LocalJumpError, потому что это кажется актуальным, и потому что (я думаю!) нет никакого возможного кода Ruby, который приведет к тому, что LocalJumpError будет "естественно" поднят в этом контексте. Если это окажется ложным, вы можете легко заменить свой собственный новый класс исключений.