Демистификация продолжений в Ruby (потому что не нужно их бояться).
Многие люди и близко не подходят к продолжениям, и, вероятно, поэтому они были исключены из JRuby и pseudo-deprecated в Ruby 1.9 (хотя, производительность YARV также сказалась в этом вопросе) путём выноса во внешнюю библиотеку с требованием явного вызова "require 'continuation'". На первый взгляд это непонятно, но есть секрет, который никто не знает о продолжениях: они чертовски просты. НЕВЕРОЯТНО просты.
Большинство блогов и статей, которые я видел, пробовали объяснить продолжения, делают это неправильно. Они утомляют вас низкоуровневыми подробностями, которые в самом начале вводят вас в ступор, или вы боитесь начать из-за предпосылки "это сложно, но я попробую вам объяснить". Я не собираюсь так делать, я намерен объяснить вам продолжения в двух словах, и я хочу, чтобы вас не пробило на смех, плач или рвоту, когда вы прочтёте это:
Знаменитые GOTO
О, нет. Я только что сказал плохое слово, не так ли? У вас, должно быть, есть собственный иррациональный страх перед ключевым словом goto, но вы хотя бы знаете, что это и как оно работает:
#include <stdio.h>
int main() {
int i = 0;
printf("%d\n", i);
goto some_label;
i = 1;
some_label:
printf("%d\n", i);
return 0;
}
Рабочий кусок кода на С показывает как мы можем перейти в другую точку в функции. Как вы и ожидаете, он напечатает дважды 1, а не 0 и затем 1. Эволюция языков привела к отказу от такой формы программирования через вызов методов и обработку исключений. Новые практики - это на самом деле синтаксический сахар поверх старой концепции goto. К счастью, те, кто смеялись над goto и читали нотации, в один прекрасный день сядут и осознают, как на самом деле работает центральная часть компьютера, но это уже офтопик. Фишка в том, неважно используете вы goto или нет, что они очень просты для понимания. По факту они так же просты как и продолжения: давайте перепишем код с использованием наших "страшных" ребят:
def main
i = 0
callcc do |label| # callcc даёт нам ‘label’ - объект продолжения
puts i
label.call # это наш оператор goto
i = 1 # это мы пропускаем
end # а здесь фактическое местоположение метки label
puts i
end
Для тех, кто в курсе, этот код не показывает всех возможностей callcc (это будет позже), но вы можете рассматривать "label.call" как эквивалент команды "goto label", а окончание callcc-блока - как место, где метка label должна фактически находиться. Это основы того, как работают продолжения.
Если для вас проблема прочесть код выше - то небольшой трюк для понимания заключается в том, что код внутри блока callcc выполняется немедленно, один и только один раз. Это просто, так что вы можете выполнять разовую инцициализацию с помощью продолжений. Окончание блока (не сам блок) - это место, куда будет переходить управление от последующих вызовов продолжения.
Теперь вы знаете
Что, то настолько просто? Продолжения - это ещё один способ написать goto? Ответ: да, и даже больше. В действительности есть только два функциональных отличия, которые делают их более мощными, чем приведённые в примере выше C-goto, и одно, делающее менее мощным.
Более мощные? Как?
1. Они не локальны для вашего метода. Проще говоря, вот это сделать в С мы не можем:
// a() вызвана из main()
void a() {
printf("hello world\n");
label1:
printf("then you say…\n");
b();
}
void b() {
printf("then I say…\n");
goto label1;
}
Это должно напечатать "Hello world", затем "then you say", "then I say" в бесконечном цикле. Проблема в том, что этот код не скомпилируется. Не вдаваясь в подробности, почему С этого не делает, я просто покажу, как мы можем сделать рабочий код с использованием продолжений:
def a
puts "hello world"
callcc {|cc| $label1 = cc } # делаем вид, что это "label1:"
puts "then you say…"
b
end
def b
puts "then I say"
$label1.call # делаем вид, что это "goto label1"
end
Как показывают комментарии, это почит один-в-один трансляция кода С выше. Наши объекты продолжений привязаны к глобальным переменным, что делает их "глобальными метками", в которые можно перейти по вызову метода call (представьте, что это "label.goto" вместо #call).
2. Они хранят состояние стека. Причина, почему метки в C локальны в функции, - потому что вы не можете переходить между функциями без изменения состояния стека. Вообще говоря, в С есть пара трюков, чтобы делать это (называются longjmp и setjmp, для перехода в произвольные адреса и продолжения выполнения, которые действуют как продолжения, выполненные для C). Но мы фокусируемся на Ruby. Основа - что объект продолжения генерируется с помощью callcc ( {|cc| something } ) и содержит снимок стека в момент своей генерации (запомните, что блок something выполняется только в первый раз). Это позволяет нам переходить между методами в классе, между классами и переходить назад из вложенных вызовов любой глубины стека.
Что насчёт реального примера?
Последняя ситуация с использованием в реальном мире. Представьте, что мы пишем веб-фреймворк в Rails-стиле с использованием before_filter и мы хотим возможность прерывать выполнение во время исполнения фильтра и прыгать назад к коду роутера чтобы найти следующий совпадающий роут. Sinatra делает это методом pass, без использования продолжений, этот концепт является отличным примером желания произвольного перехода назад по стеку. Мы можем быть на 3 вызова по глубине или на 20 вызовов, но нам нужно перейти в специфическую точку в потоке выполнения программы. Обычно люди имплементируют это в стиле обработки исключений try/catch (возможно RouteAbortError), но продолжения могут быть немного чище в зависимости от сценария (возможно у нас есть метод для перехватывания исключений на 3 уровня по стеку вверх, а нам нужно перейти на 7 вызовов вверх к начальной точке).
Менее мощные? Как?
Вероятно, вы поняли из начального примера, что переход вперёд отличается от перехода назад. Ограничение в том, что продолжения не могут служить для перехода вперёд, во всяком случае не для перехода между между методами. Причина в том, что в C метки компилируются в программе статически, но в Ruby объекты продолжений создаются в рантайме. Это означает, что сперва нужно выполнить callcc в нашем будущем методе чтобы создать продолжение, но мы не можем запустить код, который ещё не был запущен. Короче говоря, продолжения отлично подходят для возвращения во времени, но не для прыжков вперёд.
В заключение
Итак я сначала назвал продолжения "знаменитые goto". Это удачно даёт вам понимание просто концепта их использования, но теперь я должен признаться и немного изменить определение, чтобы сделать его более точным. Вместо знаменитых goto думайте о продолжении как о ЗНАМЕНИТЫХ, СОХРАНЯЮЩИХ СТЕК GOTO ДЛЯ ПЕРЕХОДОВ НАЗАД
Теперь, когда вы знаете вокруг чего вся эта суета, вы можете сами решить, ненавидеть продожения или нет.
Как вы и ожидаете, он напечатает дважды 1, а не 0 и затем 1.
дважды 0 конечно же