Изменение программы во время ее работы

Не уверен, что это проблема emacs-SLIME или проблема CL или SBCL.

Я слышал, что интерактивная природа Lisp позволяет изменять программу во время ее работы. Не зная специфики того, что подразумевается под этим, я попробовал следующее, поместив это в отдельный файл:

(defparameter repl-test-var 5)
(defun repl-test ()
  (format t "repl-test-var is: ~a" repl-test-var)
  (fresh-line)
  (when (not (equal (read-line) "quit"))
    (repl-test)))

Затем я компилирую и запускаю (repl-test) и каждый раз, когда я нажимаю Enter, я вижу номер 5,

Без ввода quit в REPL я возвращаюсь к своему файлу и изменяю 5 к 6 и снова скомпилировать. Вернувшись к REPL, нажатие Enter по-прежнему показывает 5, Если я наберу quit а потом беги (repl-test) опять же, теперь я вижу 6,

Я также попытался загрузить, а также комбинацию компиляции с последующей загрузкой с использованием ярлыков SLIME, и они также не имеют никакого эффекта до тех пор, пока я не выйду из запущенной программы и не запустлю ее снова.

То, что я пытаюсь сделать, либо невозможно, либо требует еще одного шага в коде?

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

4 ответа

Решение

Похоже, что ваш код не перезагружается, когда REPL занят, потому что ваш SBCL-образ является однопоточным. Вы можете определить, что ваш SBCL является однопоточным, проверив, что:sb-thread не присутствует в *features*. Threaded vs unhreaded определяется при компиляции самого SBCL, поэтому, чтобы получить желаемое поведение, вам нужно либо получить двоичный файл SBCL с включенными потоками, либо скомпилировать SBCL с включенными потоками.

Отсутствие потоков может помешать некоторым преимуществам интерактивной разработки (как в вашем тесте, или если вы хотите разработать веб-программу, в которой серверный компонент работает в том же образе), но некоторые преимущества остаются открытыми. Некоторые полезные аспекты интерактивной разработки, которые не требуют, чтобы ваша программа активно "делала" что-либо для вас, чтобы наслаждаться ими, включают в себя то, что вам нужно только перезагрузить части вашей программы, которые вы изменили, что эта перезагрузка не заставляет программу сбросьте данные, которые он загрузил (как может быть при перезапуске), и что REPL можно использовать в качестве удобного окна в состоянии и поведении вашей программы.

Когда функции заменяются в Лиспе, это не означает "самоизменяющийся код".

Если функция выполняется в Лиспе, указатель инструкции содержит ссылку на функцию, и поэтому функция продолжает оставаться живым объектом, который не может быть восстановлен сборщиком мусора.

Когда вы переопределяете функцию, это означает, что новый объект функции связан с именем. Когда функция вызывается по имени, используется новая функция.

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

Это очень похоже на "последнее закрытие" в Unix для открытого файла, который был удален из структуры каталогов.

Проблема проявляется не только в нескольких потоках, но и в простой рекурсии. Если функция, которая выполняет сама себя, инициирует переопределение, то функция будет продолжена со старым телом. Более того, Lisp позволяет самозвонкам в рекурсивных функциях избегать связывания имен, но использовать прямой механизм. Если рекурсивная функция переопределяет себя, рекурсивные вызовы, все еще выполняемые в одном и том же вызове, могут продолжать поступать в одно и то же тело.

В более общем смысле Common Lisp позволяет компиляторам генерировать эффективные вызовы среди функций, находящихся в одном файле. Таким образом, вы обычно должны думать о замене исполняемого кода как о уровне модуля, а не как о уровне отдельных функций. Если функции A и B находятся в одном и том же модуле, и A вызывает B, тогда, если вы просто замените B без замены A, A может продолжить вызывать старый B (потому что B был встроен в A, или потому что A не проходит через символ, но использует более прямой адрес для B). Вы можете объявить функции notinline подавить это.

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

Чтобы обновить работающую программу, вы должны найти способ взаимодействия с запущенным изображением lisp во время работы вашей программы. Это можно сделать с помощью многопоточности или с помощью отладчика.

Чтобы использовать многопоточность: попробуйте запустить функцию следующим образом:

(defparameter *thread* (sb-thread:make-thread #'repl-test))

При использовании emacs + slime: Проверить функцию в *inferior-lisp* буфера, и измените его в *slime-repl sbcl* буфер.

Еще одна тестовая программа, которая демонстрирует изменение работающей программы:

(defun update (i) 
   (+ i 1))

(defun hotpatch-test ()
       (loop for i = 0 then (update i) do
       (format t "~&i = ~d~%" i)
       (fresh-line)
       (sleep 5)))

Начни с

(defparameter *thread* (sb-thread:make-thread #'hotpatch-test))

Наблюдайте за печатными числами, затем измените определение update например как

(defun update (i)
   (+ i 2))

и посмотрите, как меняется последовательность вывода чисел.

Наконец, поток может быть убит

(sb-thread:terminate-thread *thread*)

Обновить:

Другой способ обновить запущенную программу без использования многопроцессорной обработки - это прервать программу с помощью C-c (или же C-c C-c в слизь), загрузить / ввести новый код в отладчике, а затем выбрал continue перезапустите, чтобы продолжить запуск программы с того места, где она была прервана.

Сам Emacs является великолепным примером этого. Изменить определение функции (возможно, не что-то решающее, например, car или же self-insert-command!:-) и смотри, как меняется его поведение. Смотрите также, в частности, совет Emacs.

Скомпилированная программа на Лиспе по определению не запускает интерактивный REPL, поэтому не демонстрирует это поведение "из коробки".

Проблема с вашим примером кода похожа. Он связывает REPL, поэтому нет простого способа изменить среду программы во время ее работы.

Что делает Lisp настолько универсальным (хотя и не уникальным), так это то, что eval; (б) очень легко написать свой собственный REPL поверх него; и (c) лучшие из них также предлагают документацию и / или зацепки для изменения и расширения встроенного REPL.

Более полезный пример программы eval некоторый ввод (ввод с клавиатуры? Дисковый файл? Аутентифицированная загрузка?), пока он продолжает работать.

За пределами "секретного соуса", который evalЛегко найти примеры программ, которые позволяют, скажем, обновить плагин, в то время как программа продолжает выполнять скомпилированный код, но Lisp не предоставляет никаких специальных возможностей для этого - программа должна быть собрана для поддержки этот.

Другие вопросы по тегам