Спасибо, прекрасная статья для ознакомления с совеременным состоянием DL (и отдельное спасибо за ссылки)!
Ждем вторую часть (надеюсь, техническую и понятную, а то сейчас все больше поверхностных обзоров успешных архитектур и проектов, а не статей с серьезным их описанием и анализом).
Scheme — это мощный язык. Не совсем правильно говорить о возможности «нескольких продолжений». Когда интерпретатор встречает call/cc, он просто в явном виде даёт в руки программисту текущее состояние, передавая его как аргумент в лямбда-функцию. Дальше — дело программиста, что с этим состоянием сделать. Распространённой практикой является сохранить его, как описано в статье.
И, конечно, ничего не мешает вам рассовать call/cc по разным местам программы и сохранять состояния в разных переменных. Это просто переменные, такие же, как и любые другие. Тот факт, что в каждой них сохранён некоторый контекст — это воля программиста.
К сожалению, ничего не слышал о meep, mpb; ознакомлюсь, если будет время.
Да, с одной стороны, есть некоторая схожесть. Хотя у меня gsave/grestore вызывают скорее ассоциацию с обычными функциями в процедурном программировании. Позвали функцию — она сделала изолированно свою работу независимо от нас, ничего не поменяв (только вывела что-то на экран). Понятно, что функция — значительно более простая конструкция, чем call/cc.
Сожалею, что так и не смог помочь. Общий принцип работы интерпретатора достаточно прост, совпадает с логикой работы интерпретатора любого функционального языка (в общих чертах).
Представьте, что я не рассказывал бы о продолжениях вообще:
Сначала распечатается результат test-func, затем выполнится sample-function. Программа завершит свою работу.
Теперь же вернёмся к нашему случаю (кажется, я совершил ошибку, обернув (*env* #t) никогда больше не осуществляющимся вызовом (display) — это лишь затруднило понимание):
Она стала совсем простой! Выполняется этот код обычным образом. Потом доходит до (*env* #t) и этот код снова уничтожается, заменившись, правда, таким же. Так и получается «бесконечный цикл»!
Когда мы совершаем прыжок на сохранённое состояние, мы, по сути, уничтожаем всё (кроме, конечно, переменных типа самой *env*), с чем работали, оказываясь в новой программе (часто пытаются описать это параллельной реальностью, прыжком во времени, и т.п.; я сознательно пострался избегать этого в статье, чтобы не создавать ложных ассоциаций, а попытаться донести, что же происходит на самом деле). Уничтожается стек вызова, подменяется тем стеком, который мы сохранили ранее. При этом с собой мы можем забрать один единственный кусок value, который «подменит» в «новой программе» часть кода, которая была обёрнута call/cc констуркцией.
Откуда появилось (display (*env* #t))?
Такого в коде нет. Возрат к сохранённому состоянию осуществляется строкой кода (*env* #t). В этот момент мы, по сути, начинаем выполнять «другую» программу, сохранённую в *env*. Её «текст» выглядит так:
Начинается всё с вызова функции (display ...), которая вызывает test-func. Далее, test-func вызывает вычисление блока (if ...). Теперь, внимание, внутри условия if-a мы сохраняем текущее состояние с помощью (set! *env* cc). Т.е. теперь, если мы когда-нибудь вызовем (*env* value), то мы окажемся именно тут (****), «внутри» условия if-a. Но вместо вычисления этого условия будет подставлено значение value. Запомним это, двигаемся дальше.
test-func спокойно завершит свою работу ((> 3 5) вернёт #f, if вернёт "false ", ну а test-func вернёт этот результат выше). display получит этот результат, распечатает его на экране и тоже завершит свою работу.
Далее вызывается (*env* #t). Мы переносимся в состояние ****! Всё идёт по-новой, точно так же, за тем лишь исключением, что условие if-a вычислено не будет, вместо него будет подставлено #t и поэтому в конце-концов будет распечатано "true ".
А потом снова будет (*env* #t)…
Честно — не знаю, как объяснить по-другому, но если всё-таки будет непонятно, я напрягусь и придумаю что-нибудь ещё.
Спасибо, прекрасная статья для ознакомления с совеременным состоянием DL (и отдельное спасибо за ссылки)!
Ждем вторую часть (надеюсь, техническую и понятную, а то сейчас все больше поверхностных обзоров успешных архитектур и проектов, а не статей с серьезным их описанием и анализом).
И, конечно, ничего не мешает вам рассовать call/cc по разным местам программы и сохранять состояния в разных переменных. Это просто переменные, такие же, как и любые другие. Тот факт, что в каждой них сохранён некоторый контекст — это воля программиста.
Да, с одной стороны, есть некоторая схожесть. Хотя у меня gsave/grestore вызывают скорее ассоциацию с обычными функциями в процедурном программировании. Позвали функцию — она сделала изолированно свою работу независимо от нас, ничего не поменяв (только вывела что-то на экран). Понятно, что функция — значительно более простая конструкция, чем call/cc.
Представьте, что я не рассказывал бы о продолжениях вообще:
Сначала распечатается результат test-func, затем выполнится sample-function. Программа завершит свою работу.
Теперь же вернёмся к нашему случаю (кажется, я совершил ошибку, обернув (*env* #t) никогда больше не осуществляющимся вызовом (display) — это лишь затруднило понимание):
При вызове (*env* #t), грубо говоря, весь существующий код исчезает. Мы попадаем в такую программу:
Она стала совсем простой! Выполняется этот код обычным образом. Потом доходит до (*env* #t) и этот код снова уничтожается, заменившись, правда, таким же. Так и получается «бесконечный цикл»!
*env*
), с чем работали, оказываясь в новой программе (часто пытаются описать это параллельной реальностью, прыжком во времени, и т.п.; я сознательно пострался избегать этого в статье, чтобы не создавать ложных ассоциаций, а попытаться донести, что же происходит на самом деле). Уничтожается стек вызова, подменяется тем стеком, который мы сохранили ранее. При этом с собой мы можем забрать один единственный кусокvalue
, который «подменит» в «новой программе» часть кода, которая была обёрнута call/cc констуркцией.(display (*env* #t))
?Такого в коде нет. Возрат к сохранённому состоянию осуществляется строкой кода
(*env* #t)
. В этот момент мы, по сути, начинаем выполнять «другую» программу, сохранённую в*env*
. Её «текст» выглядит так:В этом «коде» value — это то значение, которое мы передали при вызове:
Поскольку в нашем случае
value
— это#t
, то фактически исполняться будет всё вот так:if, разумеется, вернёт строчку
"true "
, она и будет распечтана. после этого интерпретатор пойдёт дальше по тексту, снова встретив(*env* #t)
Рассмотрим процесс интерпретации кода из соответствующего примера.
Начинается всё с вызова функции
(display ...)
, которая вызываетtest-func
. Далее,test-func
вызывает вычисление блока(if ...)
. Теперь, внимание, внутри условия if-a мы сохраняем текущее состояние с помощью(set! *env* cc)
. Т.е. теперь, если мы когда-нибудь вызовем(*env* value)
, то мы окажемся именно тут (****), «внутри» условия if-a. Но вместо вычисления этого условия будет подставлено значениеvalue
. Запомним это, двигаемся дальше.test-func
спокойно завершит свою работу ((> 3 5)
вернёт#f
,if
вернёт"false "
, ну аtest-func
вернёт этот результат выше).display
получит этот результат, распечатает его на экране и тоже завершит свою работу.Далее вызывается
(*env* #t)
. Мы переносимся в состояние ****! Всё идёт по-новой, точно так же, за тем лишь исключением, что условие if-a вычислено не будет, вместо него будет подставлено#t
и поэтому в конце-концов будет распечатано"true "
.А потом снова будет
(*env* #t)
…Честно — не знаю, как объяснить по-другому, но если всё-таки будет непонятно, я напрягусь и придумаю что-нибудь ещё.
Удачи вам в изучении Scheme!
Хорошо, обязательно будет статья!