Изменение программы во время ее работы
Не уверен, что это проблема 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 не предоставляет никаких специальных возможностей для этого - программа должна быть собрана для поддержки этот.