Проклятый Common Lisp

Ещё в июле я, добравшись до описания ввода-вывода в SWI-Prolog, понял, что вид этого параграфа будет сильно зависеть от того, что получится в параграфах, посвящённых вводу-выводу в Common Lisp и Scheme; самих этих параграфов на тот момент не существовало, я оставил их "на потом" вместе с тонкостями эксплуатации интерпретаторов.

Сказать, что создание соответствующего параграфа пошло через гадину — это ничего не сказать. Сегодня я ввод-вывод Common Lisp, наконец, добил, заодно убедившись, что "стандарт" Common Lisp, как и положено стандартам, ни на что не годится, но, в отличие от других языков, для Common Lisp создатели конкретных реализаций пошли дальше стандартизаторов и сотворили нечто ещё хуже, чем исходный стандарт. Рассматривал я SBCL, ECL и GCL; чтобы объяснить, как написать простую программу, которая всего-навсего не вываливается в отладчик при возникновении ошибки открытия файла и при нажатии Ctrl-C (и я ни фига не шучу), пришлось написать 15 страниц текста, причём до какой-то приемлемой степени это удалось только с SBCL (сообщение permission denied напечатать можно, но оно выглядит так, как хочет интерпретатор, а не программист); в ECL в объекте исключения информация о конкретной ошибке отсутствует (нет, не шучу, я серьёзен!), в GCL исключения вообще невозможно перехватить — интерпретатор вроде бы знает о существовании соответствующих примитивов, но при попытке их применить говорит, что такие функции у него не определены. Других свободно распространяемых (и при этом живых) реализаций Common Lisp я не нашёл. Похоже, Лисп всё-таки намного мертвее, чем мне бы этого хотелось.

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

UPD:Между делом, по объёму четвёртый том сравнялся с самым толстым из первых трёх (496 страниц), при этом писать там ещё довольно много. Мне уже интересно, удастся ли это всё загнать хотя бы в 600.

Это пример для

Это пример для иллюстрации обработки исключений в CL?

Если нет, то не лучше ли было сделать наоборот: сначала проверить, что файл доступен, а уж потом с ним работать? Всё-таки недоступный файл — ситуация не исключительная, а вполне штатная.

Питонисты, кстати, очень продвигают такой подход: сначала делаем без каких-либо проверок, а потом ловим ошибки. Любят по этому поводу приводить цитату от Грейс Хоппер (которая, всё-таки, несколько по другим, более армейским, поводам была сказана).

А какой подход к обработке ошибок считаете разумным вы?

Какой ЯП, такие и "программисты"

Питонисты, кстати, очень продвигают такой подход: сначала делаем без каких-либо проверок, а потом ловим ошибки.

Учитывая уровень порога входа в этот ЯП, это неудивительно. Поделия на питоне - это вообще сборник крайне кривых и глючных "решений". Питономакак вообще не смущает тот факт, что они создают "софт" на скриптовом языке, причём очень посредственном. Он даже в роли скриптового языка практически неприменим, потому что представляет собой кривое поделие.
Поэтому все "практики" кодерков на питоне - это одно сплошное недоразумение, т.к.:
1) Питон - это ущербный ЯП, который нормальный специалист даже не считает за таковой.
2) Питон используют люди, которые вообще мало что понимают в разработке ПО, судя по генерируемым ими "решениям". Не делать проверок, а вместо этого ловить ошибки - это просто лютый манкикодинг.

admin аватар

Это пример для

Это пример для иллюстрации обработки исключений в CL?

Разумеется, нет. Изначально я вообще не планировал рассматривать обработку исключений в Лиспе, особенно в CL, где она намертво завязана на CLOS — который, в свою очередь, выглядит примерно столь же нелепо, как рога у рыбы.

начала проверить, что файл доступен, а уж потом с ним работать?

Во-первых, этого сделать нельзя, ни в спецификации CL, ни в рассмотренных мной интерпретаторах нет соответствующих средств. Точнее, режим открытия probe есть, но он не решает никаких проблем: если ошибка состоит не в том, что файла не существует, а в чём-то другом — хоть тот же permission denied — то интерпретаторы впадают в истерику (длинная простыня диагностики, запуск отладчика, вот это вот всё).

Во-вторых, вы знаете такой термин race condition? Если что: в любой серьёзной конторе за это вот "сначала проверить, потом работать" вас, может, и не уволят (ибо даже программист-идиот нынче всё-таки лучше, чем незакрытая вакансия), но к важным фрагментам кода больше не допустят. Не могу удержаться от комментария, что авторы Common Lisp этого не знали (иначе им бы в голову не пришло этот вот probe придумать), что лишний раз характеризует техническую стандартизацию как явление.

Всё-таки недоступный файл — ситуация не исключительная, а вполне штатная.

Я вам больше скажу: абсолютно любая ошибка при открытии файла — это штатная ситуация. А теперь объясните это тем безмозглым тварям, которые придумали Common Lisp, и тем, которые написали SBCL, ECL и GCL.

Питонисты, кстати,

Я бы предпочёл другой подход: всем, у кого есть опыт работы на Питоне, запретить программировать вообще.

А какой подход к обработке ошибок считаете разумным вы?

Очень простой: если произошла ошибка, то библиотечная функция, в которой она произошла, обязана об этом сообщить головной программе. Всё, точка. Никаких самостоятельных действий, никакой истерии, никаких дебаггеров и т.п., всё это привилегии головной программы. А что, существует какое-то иное мнение на этот счёт?

И вот что, по-моему вы сюда зря припёрлись.

Тот мой

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

И вот что, по-моему вы сюда зря припёрлись.

Вы, Андрей, ко мне несправедливы. Я всего лишь не очень опытный программист, у которого есть по некоторым вопросам каша в голове и который при этом очень ценит ваш труд (я про книги) и ваше мнение. За каковым мнением я сюда и "припёрся". А вовсе не поспорить или потроллить, как вам, вероятно, показалось.

А за то, что вопросы возможно слишком въедливые задаю, уж извините. Стараюсь, насколько могу, быть аккуратным.

вы знаете такой термин race condition?

Знаю. Действительно, не подумал о том, что

if (file accessible) then (open file)

подвержен race condition, в отличие от

try (open file) catch (errors)

Заставили задуматься, спасибо.

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

Очевидно, в Common Lisp так и происходит: библиотечная функция сообщает об ошибке головной программе. В случае Common Lisp головной программой является интерпретатор. А далее уже интерпретатор пользуется своими привилегиями и запускает дебаггер. (У SBCL есть опция --disable-debugger и функция (disable-debugger).)

Но в целом суть проблемы понятна: Common Lisp плохо приспособлен к взаимодействию с вышестоящей операционной системой.

admin аватар

Вы, Андрей, ко

Вы, Андрей, ко мне несправедливы.

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

if (file accessible) then (open file)

Между прочим, я, пожалуй, погорячился, употребляя термин race condition. То есть он, конечно, верный в этой ситуации, но объяснить всё можно ещё проще: в системе, кроме вашей программы, есть кто-то ещё, и этот кто-то (точнее, эти кто-то, ибо их много) могут в любой момент файл стереть, переименовать, изменить права доступа и т.п.; следовательно, проверка "доступности" файла никак не гарантирует, что он останется доступен спустя те несколько микросекунд, когда наша программа таки дойдёт до open.

try (open file) catch (errors)

А этот подход лучше всего описывается характеристикой «из пушки по воробьям». Собственно говоря, создатели Common Lisp, похоже, и не предполагали, что он будет применяться в этой ситуации — они настолько не желали ни о чём думать, что всерьёз решили, будто ошибки при открытии файла можно не обрабатывать.

в Common Lisp так и происходит: библиотечная функция сообщает об ошибке головной программе. В случае Common Lisp головной программой является интерпретатор

Головная программа — это та программа, которую написал программист. Интерпретатор себя таковой считать не имеет права, а если всё-таки считает — то он ни на что не годится, и это в полной мере относится ко всем трём интерпретаторам CL, которые я успел пощупать.

У SBCL есть опция --disable-debugger и функция (disable-debugger)

эта опция даже включена по умолчанию, если SBCL использовать как интерпретатор скрипта (с опцией --script). Это никак не спасает от простыни диагностики на полтора экрана (трассы вызовов функций) и итогового падения программы при возникновении ошибки при открытии файла. Ну и, опять же, простыня диагностики при нажатии Ctrl-C — это тоже ни в какие ворота не лезет.

Common Lisp плохо приспособлен к взаимодействию с вышестоящей операционной системой.

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

общинные лиспы

GCL

Как вы его найти-то умудрились? :)

Нигде, ни в одной книге по CL, ни на одном сайте или блоге, посвящённом изучению CL, ни в одном живом обсуждении CL я упоминания (не говоря уже про рекомендацию к использованию) этого GNU Common Lisp не встречал.

Других свободно распространяемых (и при этом живых) реализаций Common Lisp я не нашёл.

Вполне жив, насколько я знаю, CCL. Есть ещё CLISP, который скорее мёртв, чем жив, однако в репозиториях многих дистрибутивов пакеты с ним до сих пор присутствуют.

Если что, я не настоящий сварщик лиспер. Так, интересуюсь чуть-чуть.

причём до какой-то приемлемой степени это удалось только с SBCL (сообщение permission denied напечатать можно, но оно выглядит так, как хочет интерпретатор, а не программист)

А дайте код посмотреть, пожалуйста, если не трудно. Интересно же.

admin аватар

CLISP мёртв, из

CLISP мёртв, из FreeBSD port collection его лет десять как удалили, причём, насколько я понял, по причине того, что в нём нашли критическую уязвимость, а фиксить её оказалось некому. Если что, мне самому он нравился больше всех (пока был жив), но увы.

Про CCL ничего сказать не могу, и, видимо, сейчас смотреть его не полезу. Глава про Common Lisp мне и так несуразно дорого обошлась (в плане времени).

GCL, как ни странно, есть в репозиториях Devuan (и, по-видимому, Debian, у них база репозиториев почти не различается).

Код на SBCL вот:

#!/usr/bin/sbcl --script

(defun do_it (file len)
    (let ((c (read-char file nil nil)))
        (cond
            ((eq c nil) t)
            ((eq c #\Newline) (prin1 len) (terpri) (do_it file 0))
            (t (do_it file (+ 1 len)))
        )
    )
)

(defun process_file (fname)
    (with-open-file (f fname :direction :input :if-does-not-exist nil)
        (if (null f)
            (progn
                (princ "Couldn't open the file" *error-output*) 
                (terpri *error-output*)
                nil
            )
            (do_it f 0)
        )
    )
)

(defun process_stdin ()
    (do_it *standard-input* 0)
)

(handler-case
    (if (cdr *posix-argv*)
        (process_file (second *posix-argv*))
        (process_stdin)
    )
    ;;
    (file-error (er)
        (princ er *error-output*)
        (terpri *error-output*)
    )
    (sb-sys:interactive-interrupt () nil)
)

Программа решает школьную задачку "читать поток до конца файла, печатать длины прочитанных строк"; если в командной строке ничего нет — читает из стандартного ввода, если же аргумент есть — открывает указанный файл и читает из него. Вся функциональность программы заключена в функции do_it, остальное — бессмысленная и беспощадная борьба с мракобесием создателей CL вообще и SBCL в частности.

CLISP мёртв, из

CLISP мёртв, из FreeBSD port collection его лет десять как удалили, причём, насколько я понял, по причине того, что в нём нашли критическую уязвимость, а фиксить её оказалось некому.

Полгода назад, прочитав это, я вам поверил на слово и пометил для себя "clisp не использовать".

А сегодня захотел посмотреть, что это за "критическая уязвимость". И обнаружил, что clisp в портах FreeBSD вполне себе жив и здоров: https://svnweb.freebsd.org/ports/head/lang/clisp/?view=log Никаких упоминаний слов vulnerability, bug или critical в логе по ссылке за 20 лет не нашёл.

Поясните, пожалуйста, а была ли уязвимость?

admin аватар

Чёрт её знает

Может, и не было. Я в своё время тоже кому-то на слово поверил.

Факт тот, что самой свежей версии нынче почти десять лет, то есть оно unsupported. В репах дебиана его не так давно не было, сейчас наметились какие-то движения, судя по https://tracker.debian.org/pkg/clisp — как я понимаю, проблема всё та же, баги там есть, фиксить их некому; ну а про "уязвимость" я, возможно, и погорячился (в конце концов, мало кто сейчас станет писать на Лиспе что-то серьёзное, о причинах см. книжку :) )

Можно сделать чуть-чуть иначе

Я тоже ненастоящий сварщик, лисп лет пятнадцать не трогал, тут просто мимо проходил. Заинтересовался примером кода, в итоге зачем-то пошёл рыться в документации sbcl. По результатам подтверждаю все Ваши тезисы про стандарты лиспа и мракобесие авторов, но хочу предложить два изменения.

У Вас в примере вот этот handler-case по сути является таким огромным try-catch на весь код. Альтернативный вариант сделать то же самое (sbcl-specific, конечно же) есть вот такой:

(setq sb-ext:*invoke-debugger-hook* 
    (lambda (condition hook) 
        (declare (ignore hook))
        (princ condition *error-output*) (terpri *error-output*) (sb-ext:exit :code 1)
    )
)

При любой ошибке выходит с ненулевым кодом, не падает в дебаггер и не печатает простыню stack trace. Ctrl+C тоже ловит (в отличие от стандартного *debugger-hook*) и позволяет по Ctrl+C тоже завершиться с ненулевым кодом.

$ sbcl --script prog.lsp input.txt 
3
1
4

$ sbcl --script prog.lsp nonexistent.txt
error opening #P"/tmp/lsp/nonexistent.txt": No such file or directory

$ sbcl --script prog.lsp 
stdin input
11

$ sbcl --script prog.lsp /dev/zero
^CInteractive interrupt at #x1001BB0595.
$ echo $?
1

$ chmod 000 input.txt 
$ sbcl --script prog.lsp input.txt 
error opening #P"/tmp/lsp/input.txt": Permission denied

Дальше, конечно, вопрос выбора между плохим вариантом и плохим, но раз уж их open не умеет возвращать ошибку, а умеет только отбрасывать condition, я бы вернулся к варианту «из пушки по воробьям» из коммента выше и сделал бы что-нибудь типа

(defun try_open (filename) ; returns stream or error
    (handler-case
        (open filename :direction :input)
	(file-error (er) er)
    )
)

и на верхнем уровне программы будет что-то типа такого:

(defun main (argv)
  (let
      ( (file (if (cdr argv) (open (second argv)) *standard-input*)) )
      (cond
          ((streamp file) (do_it file 0) (close file))
          (t (princ file *error-output*) (terpri *error-output*) (sb-ext:exit :code 1))
      )
  )
)

(main *posix-argv*)

Кажется, это чуть-чуть приятнее, чем засовывать весь код в handler-case на верхнем уровне, и позволяет лучше отделить логику программы от кода, нужного только для борьбы с sbcl.

admin аватар

Ну в принципе

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

Если на то пошло, в моём решении можно было этот if вытащить из handler-case в отдельную функцию, назвать её main, а handler-case рассматривать как "системную" обвеску.

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

Настройки просмотра комментариев

Выберите нужный метод показа комментариев и нажмите "Сохранить установки".