Как стать автором
Обновить

Комментарии 20

В рунете, пожалуй, действительно информации немного.
Хорошо, обязательно будет статья!
Ну информация есть, и в Рунете тоже. Но она так, слегка, разрозненная и сумбурная.
В любом случае, дельная статья, по этому поводу, не будет лишней.
Кстати, про shift/reset тоже было бы интересно почитать.
Не могли бы вы более подробно пояснить, откуда в первом примере (с тремя схемами) взялся цикл. На второй схеме четко видно, что его нет, а на 3-ей, где мы просто подставляем значения он появляется. Этот логический переход я не осилил. А т.к. лисп я особо не знаю, то пытаюсь понять смысл по схемам и внешнему виду кода, что не совсем эффективно.
Попытаюсь рассказать поподробнее. На самом деле всё очень просто, но сложно формулируется на естественном языке.

Рассмотрим процесс интерпретации кода из соответствующего примера.
(define test-func 
    (if (call/cc (lambda (cc) 
                  (begin
                   (set! *env* cc)
                   (> 3 5))
                 )) "true " "false "))
(display test-func)

(display (*env* #t))
(newline)


Начинается всё с вызова функции (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!

Все понятно и логично до момента «будет подставлено #t и поэтому в конце-концов будет распечатано „true“».
А дальше тот же логический переход, что и на схеме «А потом снова будет (*env* #t)…».
Но откуда там второй вызов (*env* #t)? В коде его нет.
Немного поясню, как я вижу процесс выполнения.
В вызове (display (*env* #t))
(*env* #t) выполняется и заменяется на результат своего выполнения, т.е. «true». Таким образом мы получаем выражение (display «true»). Здесь нет никакого цикла, выражение просто печатает текст на экран и завершается.
Откуда появилось (display (*env* #t))?
Такого в коде нет. Возрат к сохранённому состоянию осуществляется строкой кода (*env* #t). В этот момент мы, по сути, начинаем выполнять «другую» программу, сохранённую в *env*. Её «текст» выглядит так:

(define test-func 
    (if (value) "true " "false "))
(display test-func)

(display (*env* #t))
(newline)


В этом «коде» value — это то значение, которое мы передали при вызове:

(*env* value)


Поскольку в нашем случае value — это #t, то фактически исполняться будет всё вот так:

(define test-func 
    (if (#t) "true " "false "))
(display test-func)

(display (*env* #t))
(newline)


if, разумеется, вернёт строчку "true ", она и будет распечтана. после этого интерпретатор пойдёт дальше по тексту, снова встретив (*env* #t)
Я сейчас понял, что слишком слабо понимаю принцип работы интерпретатора lisp, а без этого дальнейшее объяснение мало чем поможет. В общем спасибо за пояснения.
Сожалею, что так и не смог помочь. Общий принцип работы интерпретатора достаточно прост, совпадает с логикой работы интерпретатора любого функционального языка (в общих чертах).

Представьте, что я не рассказывал бы о продолжениях вообще:

(define test-func 
    (if (value) "true " "false "))
(display test-func)

(sample-function)
(newline)


Сначала распечатается результат test-func, затем выполнится sample-function. Программа завершит свою работу.

Теперь же вернёмся к нашему случаю (кажется, я совершил ошибку, обернув (*env* #t) никогда больше не осуществляющимся вызовом (display) — это лишь затруднило понимание):

(define test-func 
    (if (call/cc (lambda (cc) 
                  (begin
                   (set! *env* cc)
                   (> 3 5))
                 )) "true " "false "))
(display test-func)

(*env* #t)
(newline)


При вызове (*env* #t), грубо говоря, весь существующий код исчезает. Мы попадаем в такую программу:

(define test-func 
    (if (#t) "true " "false "))
(display test-func)

(*env* #t)
(newline)


Она стала совсем простой! Выполняется этот код обычным образом. Потом доходит до (*env* #t) и этот код снова уничтожается, заменившись, правда, таким же. Так и получается «бесконечный цикл»!
Когда мы совершаем прыжок на сохранённое состояние, мы, по сути, уничтожаем всё (кроме, конечно, переменных типа самой *env*), с чем работали, оказываясь в новой программе (часто пытаются описать это параллельной реальностью, прыжком во времени, и т.п.; я сознательно пострался избегать этого в статье, чтобы не создавать ложных ассоциаций, а попытаться донести, что же происходит на самом деле). Уничтожается стек вызова, подменяется тем стеком, который мы сохранили ранее. При этом с собой мы можем забрать один единственный кусок value, который «подменит» в «новой программе» часть кода, которая была обёрнута call/cc констуркцией.
Вот это пояснение расставило всё по местам. Благодарю.
Я сначала думал, что часть вывода идет из строки
(display (*env* #t)) и это меня сбило с толку. Теперь же понятно, что весь вывод только из (display test-func), а все после (*env* #t) никогда не будет выполнено.
Спасибо! Я только сейчас понял, что наличие (display ...) лишь усложняет восприятие. Добавил информацию в статью.
Материал очень интересный и познавательный, так как некоторые open-source инструменты для научных расчетов/моделирования (meep, mpb, и др.) используют Scheme для описания входных данных и пару раз приходилось сильно извращаться чтобы правильно описать нужную систему языком Scheme. Так что будем ждать продолжения.

Мне кажется, что продолжения в Scheme напоминают по своей идеологии связку операторов gsave и grestore в языке Postscript, но в Postscript для «графических состояний» стек отдельный.
К сожалению, ничего не слышал о meep, mpb; ознакомлюсь, если будет время.

Да, с одной стороны, есть некоторая схожесть. Хотя у меня gsave/grestore вызывают скорее ассоциацию с обычными функциями в процедурном программировании. Позвали функцию — она сделала изолированно свою работу независимо от нас, ничего не поменяв (только вывела что-то на экран). Понятно, что функция — значительно более простая конструкция, чем call/cc.
meep — расчет прохождения электромагнитных волн в периодических системах методом FDTD
mpb — расчет фотонных зон и дисперсионных кривых для периодических систем
Почитать можно здесь: http://ab-initio.mit.edu/wiki/index.php/MIT_Photonic_Bands

Я правильно понимаю, что Scheme позволяет иметь несколько независимых продолжений в программе (в то время как в Postscript состояния сохраняются в отдельном стеке и восстанавливаются в соответствующем порядке)?
Scheme — это мощный язык. Не совсем правильно говорить о возможности «нескольких продолжений». Когда интерпретатор встречает call/cc, он просто в явном виде даёт в руки программисту текущее состояние, передавая его как аргумент в лямбда-функцию. Дальше — дело программиста, что с этим состоянием сделать. Распространённой практикой является сохранить его, как описано в статье.

И, конечно, ничего не мешает вам рассовать call/cc по разным местам программы и сохранять состояния в разных переменных. Это просто переменные, такие же, как и любые другие. Тот факт, что в каждой них сохранён некоторый контекст — это воля программиста.
Есть предложения, о чём написать? Или вы имеете в виду подробное описание макросов?
Да, имел в виду макросы.

Можно поступить, например, так: если генератор не может выдать очередное значение, он возвращает значение-заглушку, например, «Stop Iteration».

Как это сделать? Проверьте себя на понимание, придумайте сами (добро пожаловать в комментарии). Нужно добавить всего одну строку кода внутри (define (create-generator func) ...).

На всякий случай оставлю здесь решение, а то я как-то слишком долго над ним думал.

Нужно в конец этой лямбды добавить символ 'stop

(lambda ()
  (call/cc
    (lambda (cc)
      (set! *return* cc)
      (*env*))))
(lambda ()
  (call/cc
    (lambda (cc)
      (set! *return* cc)
      (*env*)
      'stop)))

Тогда в случае, если yield не будет вызван, генератор вернёт символ 'stop.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации