← На главную

Первые впечатления от Common Lisp

Некоторое время назад я наконец-то дочитал книжку «ANSI Common Lisp». В действительности, это уже мой второй подход к CL. До этого я пытался читать «Practical Common Lisp», но как-то не пошло. Теперь же я осилил книгу полностью (ну почти, там приложений на 100 страниц…) и даже написал какую-никакую программку.

Краткие сведения о Common Lisp:

  • Lisp был создан Джоном Маккарти в 1958 году. Common Lisp появился в 1984 с целью объединения множества появившихся к тому моменту диалектов Lisp’а. Он был стандартизован ANSI в 1994. CL считается промышленным стандартом языка Lisp.
  • Еще два активно используемых нынче диалекта Lisp’а (если не считать диалекты для Emacs, AutoCAD и тп) – это Scheme и Clojure.
  • Common Lisp часто называют языком функционального программирования, но строго говоря он таковым не является. В нем есть побочные эффекты, изменяемые данные, традиционные циклы, ссылки, глобальные переменные и даже полноценное ООП. Однако существенная часть кода обычно все же пишется в функциональном стиле.
  • В CL используется строгая динамическая типизация, а также есть опциональная декларация типов. Есть автоматическое управление памятью. Все переменные являются ссылками на данные. Например, в коде (progn (setf x (list 1 2 3)) (setf y x) (setf (car x) 4) y) переменные x и y ссылаются на один список. При изменении первого элемента списка x также изменяется и список y.
  • Синтаксис CL очень прост, но одновременно и несколько непривычен большинству программистов. Выражения представляются в префиксной записи: (форма аргумент1 аргумент2 ...), где форма является специальным оператором, макросом или функцией. Аргументы могут представлять собой атомы (числа, символы) или другие выражения. Описание всего синтаксиса языка в BNF занимает строк десять.
  • Таким образом, код представляет собой обычную структуру данных CL и может быть обработан, как любые другие данные, программой на самом CL. Этим занимаются макросы. За счет макросов Lisp постоянно эволюционирует, благодаря чему и остается на плаву вот уже более 50-и лет. Похвастаться тем же может разве что Fortran.
  • CL является интерпретируемым и компилируемым языком. Скомпилированная программа не зависит от наличия интерпретатора, определенной виртуальной машины и тп.
  • В CL можно удобно работать с комплексными и рациональными числами без использования каких-либо библиотек: (= (+ 1/2 2/3) 7/6).
  • Существует транслятор Common Lisp в Си, а также компилятор CL для Android и iOS.

Что удивительно, Common Lisp при его динамической типизации является числодробилкой не хуже, чем Java или Haskell:

Бенчмарк Common Lisp

Чтобы немного попрактиковаться в Common Lisp, я попробовал решить с его помощью задачу о дне системного администратора. Решение этой задачи на Erlang вы найдете в этой заметке, а на Perl и Haskell – в этой.

; Определяем, на какое число приходится ; день системного администратора в этом году. ; (c) Alexander Alexeev 2013 | http://eax.me/ ; является ли год весокосным (defun leap-year? (y) (cond ((= 0 (mod y 400)) t) ((= 0 (mod y 100)) nil) ((= 0 (mod y 4)) t) (t nil))) ; сколько дней в заданном году (defun days-in-year (y) (if (leap-year? y) 366 365)) ; количество дней в месяце в зависимости от типа года (defun days-in-month (month leap) (cond ((member month '(april june september november)) 30) ((eql month 'february) (if leap 29 28)) (t 31))) ; готового аналога range в CL не предусмотрено :( (defun range (max &key (min 1) (step 1)) (loop for n from min to max by step collect n)) ; список пар месяц:номер (defun months-numbers () (mapcar #'cons '(january february march april may june july august september october november december) (range 12))) ; получение номера месяца (defun month->number (m) (cdar (remove-if (lambda (x) (not (eql (car x) m))) (months-numbers)))) ; получение месяца по его номеру (defun number->month (n) (caar (remove-if (lambda (x) (/= (cdr x) n)) (months-numbers)))) ; список месяцев, которые идут перед данным (defun months-before (m) (if (eql m 'january) '() (let ( (prev (number->month (- (month->number m) 1))) ) (cons prev (months-before prev))))) ; число дней с 1-го января 1-го года (не включая) (defun days-from-epoch (y m d) (let ((is-leap (leap-year? y))) (+ (reduce #'+ (map 'list #'days-in-year (range (- y 1)))) (reduce #'+ (map 'list (lambda (x) (days-in-month x is-leap)) (months-before m))) (- d 1)))) ; определяем день недели, 0 - понедельник, 6 - воскресенье (defun day-of-week (y m d) (mod (days-from-epoch y m d) 7)) ; на какое число приходится день сисадмина в заданном году (defun sysadmin-day-in-year (y) (car (last (remove-if (lambda (x) (/= 4 (apply #'day-of-week x))) (map 'list (lambda (x) (list y 'july x)) (range 31)))))) (defun main () (format t "Поздравить знакомых админов: ~S~%" (sysadmin-day-in-year 2013)))

Разрабатывалось все это в связке из оконного менеджера i3, текстового редактора vim, компилятора sbcl и утилиты rlwrap. В целом получилось довольно удобно, хотя я слышал, что гуру Lisp’ов предпочитают писать код в Emacs.

Приведенный код собирается командой:

sbcl --load sysadmin-day.lisp --eval "(sb-ext:save-lisp-and-die ⏎ \"sysadmin-day\" :executable t :toplevel 'main)"

Узнать больше о Common Lisp можно по следующим ссылкам:

У меня сложилось какое-то двоякое впечатление о Common Lisp. CL – он почти как Python, только быстрый и безо всяких там GIL, поддерживает макросы и имеет больший уклон в сторону ФП. Писать на нем можно, несмотря на скобочки, странные имена функций типа cdr и тп. Вот я только не уверен, нужно ли.

Вакансий, связанных с CL, похоже, нет вообще и в обозримом будущем много не предвидится. То есть, если и заниматься Common Lisp, то исключительно в качестве хобби. Но не лучше ли тратить свободное время, например, на Haskell? Ведь он ничем не хуже, но при этом, кажется, куда актуальнее.