Если вы не слышали о call/cc, то вам определённо стоит познакомиться с этим мощным инструментом! Поговорим о продолжении (call/cc), простой, но трудно понимаемой конструкции, обладающей огромной силой в правильных руках. Реализуем с их помощью механизм yield/next/for… in, аналогичный таковому в Python. Обернём внутренности с помощью макроса — ещё одного интересного механизма Scheme.

Статья ориентирована на начинающих программистов. Лисперы вряд ли почерпнут что-то новое, но я буду благодарен за найденные ошибки.

image


Что и зачем


Каждый программист периодически сталкивается с недостаточной выразительностью своего кода, когда простая красивая идея требует написания сотен строк. Это всегда подталкивало на создание новых, более экспрессивных инструментов таких, как ФП, с гораздо большей плотностью смысла на единицу текста программы.

Продолжение (continuation) — это один из механизмов, позволяющих программисту на ходу создавать такие инструменты. История call/cc (call-with-current-continuation, синтаксическая конструкция, отражающая идею продолжений) тесно связана со Scheme, не самым популярным языком, который, однако, несколько десятков лет служит источником вдохновения для программистов. Поэтому повествование будет вестись языком Scheme, а все примеры кода предназначены для интерперетации с помощью Guile (уверен, что с почти нулевыми усилиями заведётся в Racket, Chicken и, наверное, в сотне других интерпретаторов этого диалекта Лиспа).

Часть I. Продолжения


Начало (суть продолжений)


Продолжение — это страший брат GoTo, оператора, который нельзя использовать. Продолжение даёт возможность:

  • Получить (захватить) состояние программы в данный момент
  • Сохранить это состояние (есть и другие варианты)
  • Вернуться к этому состоянию впоследствии, когда бы вы ни захотели

Но зачем возвращаться к прошлому состоянию?

  • Чтобы снова пойти вперёд, организовав цикл. Это довольно наивное применение (фактически, использование GoTo).
  • Чтобы понять реальную мощь продолжений, нужно выяснить, что же означает «состояние программы», упомянутое выше. В действительности сохраняется текущий стек вызова функций, т.е текущий контекст. А вот следующая функция, которая должна вернуть значение предыдущей (т.е. та, которую мы фактически оборачиваем конструкцией call/cc — см. ниже), не сохраняется. Впоследствие её можно заменить другим кодом (в частности, некоторой константой). Представьте, что вы можете вернуться к себе из будущего и передать себе прошлому какие-либо знания/материальные объекты/указания для дальнейших действий!

Поясним сказанное на практике:

Представим себе некоторый кусок программы. Функция 1 вызывает функцию 2, та вызывает функцию 3 от каких-то других переменных. Перед вызовом, скажем, функции 2, сохраним состояние (называемое текущим контекстом). Впоследствии в любой момент мы можем вернуться к этому контексту, подменив результат работы цепочки функций (func2 (func3 a b c)) нужным нам значением, например, d.

Проверим, что всё работает действительно так.

Первый пример


Создадим какой-нибудь сферический пример в вакууме. Определим функцию test-func:

; некоторая функция для примера
(define test-func 
    (if (> 3 5) "true " "false "))

(display test-func)
(newline)

Результат (очевидный):

>> false

Теперь сохраним контекст перед вычислением условия в if. Посмотрим, как это сделать:

(call/cc 
   (lambda (cc) (Some actions)))

Появление call/cc требует от интерпретатора, чтобы он взял текущий контекст и передал его в лямбда-функцию, опеределённую нами внутри. Лямбда-функция принимает один аргумент (здесь и далее в текстах программ будем называть его cc — сокращённо от «current continuation»). Мы можем сохранить этот контекст:

(define *env* #f)
(call/cc 
   (lambda (cc) 
      (begin
         (set! *env* cc) ; Берём контекст cc, и сохраняем его в переменную *env*
         (Some actions))))

Теперь совершим магию. Возьмём сохранённый контекст, и перейдём к нему. Мы обернули конструкцией call/cc условие в блоке if, поэтому нужно при вызове контекста передать значение, которое должно быть возвращено вместо вычисления (> 3 5).

Это делается так:

(*env* Any_value)

Здесь на месте «Any value» может стоять любой код, возвращающий некоторое значение, или само это значение.

Теперь, если мы напишем:

(*env* #t)

мы вернёмся к точке, где был получен контекст, и всё будет выглядеть для внешнего по отношению к блоку (call/cc ...) коду так, как будто функция, находящаяся внутри этого блока (условие if), вернула #t!

Итак, результат


UPDATE:

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

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

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

>> false true true true true true true true true true true true true true true true true true true true true true true true true true true true ... 

Всё работает так, как мы хотели, но… бесконечный цикл?! Спокойно, всё в согласии с теорией. Разбёрём, как работает этот пример.



Мы возвращаемся в состояние программы где-то внутри стека вызова, порождённого (display ...). Там мы подставляем угодный нам #t, влияющий на результат (выводится true), производится благополучный выход из (display ...), вызывается (*env* #t), и по-новой…

Делаем свой генератор


Первое знакомство с call/cc вызывает понимание силы этого инструмента, но далеко не сразу очевидно, как её использовать и что можно реализовать. Классический список вещей, реализуемых с помощью call/cc, включает циклы, выход из цикла или рекурсии, выход из цикла или рекурсии с возвратом, сопрограммы и коопреативную многозадачность. Таким образом, продолжения способны изменять поток выполннения программы всеми мыслимыми способами.

Попробуем использовать эту возможность для реализации на языке Scheme аналога генераторов в Python. Потребуем, чтобы в результате работало так:

; Определяем функцию генератора
(define-generator (generator-func arg1 arg2 ...)
   (...
       (yield result) ; Возвращаем очередное значение
   ...))

; Определяем значения аргументов. Теперь my-gen — генератор:
(define my-gen (generator-func 10 30 ...))

(display my-gen) ; Печатает первое значение
(display my-gen) ; Печатает второе значение

; Или распечатаем всё, что есть
(for item in (generator-func 100 70 ...)
   (display item))

Возможно, не так лаконично, как в Python, но Scheme всё же ограничена своим синтаксисом (что не мешает быть ему предельно простым и невероятно универсальным).

Первые заготовки


Реализация функции yield почти очевидна. Нужно сохранить текущий контекст (чтобы потом продолжить с него), затем сделать прыжок туда, откуда был вызван генератор — и вместо этого генератора подставить возвращённое с помощью yield значение:

(define (yield value)
   (call/cc
      (lambda (cc)
           (set! *env* cc) ; сохраним контекст
           (*return* value)))) ; совершим прыжок в контекст, сохранённый ранее, с подставновкой value

  • Сразу понятно, что у каждого генератора (их может быть несколько в программе), должны быть свои *env* и *return*, сохранённые внутри некоторой локальной области видимости.
  • Из первого следует, что yield не может быть глобальным, а значит, его нужно передать функции, которая вызывает yield

Реализуем это, заодно написав пример генератора квадратов чисел от 1 до n:

(define (create-generator func)
   (define *env* (lambda () (func yield)))
   (define *return* #f)

   (define (yield value)
      (call/cc
          (lambda (cc)
              (set! *env* cc) 
              (*return* value))))
   
   (lambda () 
      (здесь будет логика генератора)))

; Генератор квадратов чисел
(define (squares-gen n)
    (lambda (yield)
        (let loop ((n (+ n 1)) (k n))
          (if (> k 0)
            (begin
               (yield (expt (- n k) 2))
               (loop n (- k 1)))))))

Почти готово


Дело осталось за малым: нужно записать что-то в переменную *return*. Чтобы вызванный в очередной раз генератор отдал значение, нужно сохранить контекст в самом начале генератора, чтобы потом подменить его внутреннюю часть возвращаемым значением. Именно об этом картинка из начала поста:

image


Мы находимся в некоторой части программы и хотим идти дальше, вперёд. Но для этого нужно получить очередное значение от генератора (ящик c). Заходим в генератор (сохраняем состояние и поднимаемся по лестнице), подбираем ящик и «телепортируемся» обратно (возвращаемся к сохранённому состоянию), но уже с ящиком! На деле нужно добавить пару строк:

(define (create-generator func)
   (define ...)
        ...
   (lambda ()
        (call/cc
           (lambda (cc)
               (set! *return* cc)
               (*env*))))) ; Изначально просто входим в функцию func, впоследствии продолжаем с сохраненного места, 
                                ; подменив (yield smth) в коде func... ничем!

Результат


Соберём всё вместе:

Результат
(define (create-generator func)
   (define *env* (lambda () (func yield)))
   (define *return* #f)

   (define (yield value)
      (call/cc
          (lambda (cc)
              (set! *env* cc) 
              (*return* value))))
   
   (lambda ()
        (call/cc
           (lambda (cc)
               (set! *return* cc)
               (*env*)))))

; Генератор квадратов чисел
(define (squares-gen n)
    (lambda (yield)
        (let loop ((n (+ n 1)) (k n))
          (if (> k 0)
            (begin
               (yield (expt (- n k) 2))
               (loop n (- k 1)))))))

(define my-gen (create-generator (squares-gen 10)))

Проверяем:

; Вызовем my-gen 7 раз
(let loop ((n 7))
    (if (> n 0)
        (begin
           (display (my-gen))
           (display " ")
           (loop (- n 1)))))


>> 1 4 9 16 25 36 49 

Ура! Работает!

Замечание


Я надеюсь, вы заметили одну очевидную ошибку. Если мы вызовем полученный генератор больше 10 раз, мы войдём в бесконечный цикл. Вызывая (*env*), мы полностью возвращаемся к тому состоянию, в котором были при последней итерации, потому что не сохраняем нового, поскольку не доходим в коде функции-генератора до yield. Можно поступить, например, так: если генератор не может выдать очередное значение, он возвращает значение-заглушку, например, «Stop Iteration».

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

Часть II. Макросы


Зачем?


Мы добились нужного нам поведения. Но синтаксис совсем не тот. Чтобы создать функцию генератора, нам нужно обернуть её лямбда-функцией, сделать много лишних телодвижений… к счастью, в Scheme есть мощный механизм макросов. Макросы, как всегда, зло, если рассовывать их повсюду, но если писать один раз и надолго, почему бы не облегчить себе жизнь красивым синтаксическим сахаром?

Краткое описание


Макросы освещены на просторах интернета гораздо лучше, чем продолжения, поэтому лишь коротко оставновимся на них (вторая причина — неожиданно большой размер статьи; если сообщество посчитает нужным развёрнутое описание макросов в Scheme, я напишу вторую статью).

  • Макросы в Scheme подобны препроцессору в C. Макросы раскрываются до трансляции в байт-код.
  • Макросы преобразуют синтаксические конструкции, и только их. Мы пишем одни строки кода, на выходе другие.
  • define-syntax и иже с ним определяют правила, по которым происходит обозначенное преобразование.

Наверное, будет неправильным по сути дублировать то, что было рассказано не раз, и на Хабре в том числе.
Основы макросов в Scheme (поиск по странице: «Макросы»): habrahabr.ru/company/tcsbank/blog/267113

Здесь же мы рассмотрим тонкость, которая сыграет существенную роль.

Напишем пример макроса, который определяет функцию, просто суммирующую элементы списка (для этого макрос, конечно, не нужен; пример, как всегда, из воздуха, но необходим для понимания):

; сумматор списка конструкцией (sum (list 5 9 1 ...))
(define-syntax sum
   (syntax-rules ()
      ((_ args-list)
       (let loop ((res 0) (left-list args-list))
           (if (not (null? left-list))
               (loop (+ res (car left-list)) (cdr left-list))
               res)))))

(display (sum (list 3 4 5 1)))
(newline)

>> 13

Всё работает.

А теперь сделаем вот так:

(define loop (list 3 4 5 1))
(display (sum loop))

Представляете, в какое месиво развернётся этот макрос (из-за совпадения имён — loop, внутри макроса и снаружи)? Эх, сейчас посыпятся ошибки компиляции… Запускаем…

>> 13

Да неужели?! Как это могло сработать? На самом деле, в Scheme макрос — не такая прямая конструкция, как кажется. В нашем случае макрос развернётся в:

(let loop-1 ((res 0) (left-list loop))
  (if (not (null? left-list))
    (loop-1 (+ res (car left-list)) (cdr left-list))
    res))

Макросы умеют определять конфликты имён снаружи и внутри, поэтому внутренняя переменная loop получила другое имя, loop-1.

Syntax-case, with-syntax, datum->syntax


В нашем случае, к сожалению, такой интеллект макроса только мешает. Внутри макроса мы непременно будем использовать yield, который непременно преобразуется к yield-1.

Чтобы заставить макрос работать так, как нам нужно, существует более мощная конструкция, syntax-case.

Статья и так получилась слишком большой, поэтому подробное описание этого инструмента будет в следующей публикации (если будет необходимость).

Синтаксис схож с syntax-rules, разница в обёртке из лямбда-функции и способе возвращения значения, через (syntax something) — функция, возвращающая синтаксическую конструкцию, построенную из «something».

В Scheme существует сокращение для (syntax ...): #'.
Предыдущий пример перепишется так (причём код ниже во всех смыслах эквивалентен коду с использованием syntax-rules):

sum через syntax-case
(define-syntax sum
  (lambda (x)
   (syntax-case x ()
      ((_ args-list)
       #'(let loop ((res 0) (left-list args-list))
           (if (not (null? left-list))
               (loop (+ res (car left-list)) (cdr left-list))
               res))))))


Ввести идентификатор объекта из внешней по отношению к макросу области видимости (так сказать, из обычного Scheme) во внутреннюю область видимости макроса можно с помощью datum->syntax.

Например, для того, чтобы yield не превращался в yield-1 внутри (syntax ...), можно сделать так:

(define-syntax sum
  (lambda (x)
   (syntax-case x ()
      ((pattern)
         (syntax-case (datum->syntax x 'yield) ()
             (yield (syntax ... yield ...)))))))

Scheme предлагает некоторый синтаксический сахар, чтобы этот код выглядел приятней:

(define-syntax sum
  (lambda (x)
   (syntax-case x ()
      ((pattern)
         (with-syntax (yield (datum->syntax x 'yield))
             (yield (syntax ... yield ...)))))))

В результате мы использовали syntax-case, чтобы, по сути, создать обычный макрос, в котором мы можем без всяких проблем использовать yield, и всё будет работать так, как мы ожидаем.

Наконец, используем макросы в деле


Вспомним синтаксис, которого мы добивались:

Синтаксис
; Определяем функцию генератора
(define-generator (generator-func arg1 arg2 ...)
   (...
       (yield result) ; Возвращаем очередное значение
   ...))

; Определяем значения аргументов. Теперь my-gen -- генератор:
(define my-gen (generator-func 10 30 ...))

(display my-gen) ; Печатает первое значение
(display my-gen) ; Печатает второе значение


Создадим макрос define-generator, который создаёт функцию от аргументов arg1, arg2 ..., которая возвращает генератор. Код аналогичен тому, что мы уже написали:

; Создаём нужный макрос:
(define-syntax define-generator 
 (lambda (x) ; формальный синтаксис syntax-case
  (syntax-case x ()
   ((_ (name . args) body)
   (with-syntax ((yield (datum->syntax x 'yield))) ; введем внешний идентификатор yield в область видиости макроса
    (syntax (define (name . args) ; вернём функцию генераатора, принимающую на вход заданные аргументы, и возвращающую генератор
        (define *env* (lambda () (body))) ; здесь и далее дублируем код, написанный в статье
        (define *return* #f)

        (define (yield value)
          (call/cc
            (lambda (cc)
              (set! *env* cc) 
              (*return* value))))
   
        (lambda ()
          (call/cc
            (lambda (cc)
              (set! *return* cc)
              (*env*)))))))))))

(define-generator (squares-gen n)
    (let loop ((n (+ n 1)) (k n))
        (if (> k 0)
            (begin
               (yield (expt (- n k) 2))
               (loop n (- k 1))))))

(define my-gen (squares-gen 10))

(let loop ((n 7))
    (if (> n 0)
        (begin
           (display (my-gen))
           (display " ")
           (loop (- n 1)))))

>> 1 4 9 16 25 36 49 

Снова ура! Работает!

Послесловие


Если вдруг вы знали немного о продолжениях и макросах, а мне удалось донести до вас то, что написано выше, то вы легко по аналогии напишете реализацию for ... in ... Не забывайте про ошибку, намеренно оставленную в коде.

Спасибо, если дочитали до конца. Надеюсь, что теперь Scheme подарит ещё кому-то немного вдохновения в нашем любимом деле.
Поделиться с друзьями
-->

Комментарии (19)


  1. GlukKazan
    30.05.2016 20:28

    Думаю, развёрнутая статья по макросам не будет лишней.
    Не так уж хорошо эта тема и освещена «на просторах Интернета».


    1. gkorepanovgk
      30.05.2016 23:49
      +1

      В рунете, пожалуй, действительно информации немного.
      Хорошо, обязательно будет статья!


      1. GlukKazan
        31.05.2016 09:12

        Ну информация есть, и в Рунете тоже. Но она так, слегка, разрозненная и сумбурная.
        В любом случае, дельная статья, по этому поводу, не будет лишней.
        Кстати, про shift/reset тоже было бы интересно почитать.


  1. cs0ip
    30.05.2016 22:52

    Не могли бы вы более подробно пояснить, откуда в первом примере (с тремя схемами) взялся цикл. На второй схеме четко видно, что его нет, а на 3-ей, где мы просто подставляем значения он появляется. Этот логический переход я не осилил. А т.к. лисп я особо не знаю, то пытаюсь понять смысл по схемам и внешнему виду кода, что не совсем эффективно.


    1. gkorepanovgk
      31.05.2016 00:16
      +1

      Попытаюсь рассказать поподробнее. На самом деле всё очень просто, но сложно формулируется на естественном языке.

      Рассмотрим процесс интерпретации кода из соответствующего примера.

      (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!


      1. cs0ip
        31.05.2016 01:38

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


        1. cs0ip
          31.05.2016 01:45

          Немного поясню, как я вижу процесс выполнения.
          В вызове (display (*env* #t))
          (*env* #t) выполняется и заменяется на результат своего выполнения, т.е. «true». Таким образом мы получаем выражение (display «true»). Здесь нет никакого цикла, выражение просто печатает текст на экран и завершается.


          1. gkorepanovgk
            31.05.2016 01:59

            Откуда появилось (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)


            1. cs0ip
              31.05.2016 02:06

              Я сейчас понял, что слишком слабо понимаю принцип работы интерпретатора lisp, а без этого дальнейшее объяснение мало чем поможет. В общем спасибо за пояснения.


              1. gkorepanovgk
                31.05.2016 02:23

                Сожалею, что так и не смог помочь. Общий принцип работы интерпретатора достаточно прост, совпадает с логикой работы интерпретатора любого функционального языка (в общих чертах).

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

                (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) и этот код снова уничтожается, заменившись, правда, таким же. Так и получается «бесконечный цикл»!


            1. gkorepanovgk
              31.05.2016 02:09
              +1

              Когда мы совершаем прыжок на сохранённое состояние, мы, по сути, уничтожаем всё (кроме, конечно, переменных типа самой *env*), с чем работали, оказываясь в новой программе (часто пытаются описать это параллельной реальностью, прыжком во времени, и т.п.; я сознательно пострался избегать этого в статье, чтобы не создавать ложных ассоциаций, а попытаться донести, что же происходит на самом деле). Уничтожается стек вызова, подменяется тем стеком, который мы сохранили ранее. При этом с собой мы можем забрать один единственный кусок value, который «подменит» в «новой программе» часть кода, которая была обёрнута call/cc констуркцией.


              1. cs0ip
                31.05.2016 02:19

                Вот это пояснение расставило всё по местам. Благодарю.
                Я сначала думал, что часть вывода идет из строки
                (display (*env* #t)) и это меня сбило с толку. Теперь же понятно, что весь вывод только из (display test-func), а все после (*env* #t) никогда не будет выполнено.


                1. gkorepanovgk
                  31.05.2016 02:33

                  Спасибо! Я только сейчас понял, что наличие (display ...) лишь усложняет восприятие. Добавил информацию в статью.


  1. anandr
    31.05.2016 02:36

    Материал очень интересный и познавательный, так как некоторые open-source инструменты для научных расчетов/моделирования (meep, mpb, и др.) используют Scheme для описания входных данных и пару раз приходилось сильно извращаться чтобы правильно описать нужную систему языком Scheme. Так что будем ждать продолжения.

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


    1. gkorepanovgk
      31.05.2016 02:46

      К сожалению, ничего не слышал о meep, mpb; ознакомлюсь, если будет время.

      Да, с одной стороны, есть некоторая схожесть. Хотя у меня gsave/grestore вызывают скорее ассоциацию с обычными функциями в процедурном программировании. Позвали функцию — она сделала изолированно свою работу независимо от нас, ничего не поменяв (только вывела что-то на экран). Понятно, что функция — значительно более простая конструкция, чем call/cc.


      1. anandr
        31.05.2016 10:34

        meep — расчет прохождения электромагнитных волн в периодических системах методом FDTD
        mpb — расчет фотонных зон и дисперсионных кривых для периодических систем
        Почитать можно здесь: http://ab-initio.mit.edu/wiki/index.php/MIT_Photonic_Bands

        Я правильно понимаю, что Scheme позволяет иметь несколько независимых продолжений в программе (в то время как в Postscript состояния сохраняются в отдельном стеке и восстанавливаются в соответствующем порядке)?


        1. gkorepanovgk
          31.05.2016 13:38

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

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


    1. gkorepanovgk
      31.05.2016 02:47

      Есть предложения, о чём написать? Или вы имеете в виду подробное описание макросов?


      1. anandr
        31.05.2016 10:34

        Да, имел в виду макросы.