Блог О пользователеracket-lang

Регистрация

На странице

ЯнварьФевральМартАпрельМайИюньИюльАвгустСентябрьОктябрь (1)НоябрьДекабрь
           
12345678910111213141516171819202122232425262728293031
 

Почему Racket, а не Common Lisp


Здесь я рассмотрю отличия между Racket и Common Lisp. Отличия делятся на три группы: синтаксические (особенности синтаксиса, устраняемо), семантические (особенности работы макросов, компилятора) и технологические (что принципиально нельзя или очень сложно сделать в одном из языков).

Синтаксические отличия:

  1. Racket это Scheme, а в Scheme общее пространство для имён переменных и функций. В Common Lisp функции находятся в отдельном пространстве имён и используемое пространство определяется в каком месте выражения находится имя (если в голове списка, то функция, иначе переменная).
    Преимущество Common Lisp: можно писать (let ((list '(1 2))) (list list)) и получить корректный список '((1 2)), то есть имён доступно вдвое больше.
    Преимущество Racket: легче писать функции высшего порядка (то есть которые в качестве параметра получают функции). В Common Lisp: (defun map (func list) (if (null list) nil (append (funcall func (car list)) (map func (cdr list))))), в Racket (define (map func list) (if (null? list) null (append (func (car list)) (map func (cdr list))))). Проблемы с нехваткой имён практически нет, так как, во-первых, Racket регистрозависим, а значит можно написать let ((List '(1 2))) (list List)), во-вторых в нём очень удобная система модулей, которая практически позволяет не оглядываться при создании имён в модуле на все остальные имена.
  2. Пространство имён в пакетах/модулях. В Common Lisp всегда доступны все загруженные пакеты, что приводит к трудноуловимым ошибкам (на машине разработчика пакет установлен, но не прописан в зависимостях, у него будет всё работать, а у пользователей — если вдруг тот же пакет будет установлен).
    В Racket в каждом модуле явно указаны зависимости, при их указании можно явно указать какие имена импортировать, или кроме каких, или как переименовать при импортировании, или какой префикс добавить ко всем именам (простой метод решения конфликтов имён).
  3. В Racket есть вложенный define. Таким образом, там где в Common Lisp приходится писать
    (let ((x 1))
      (setf x (func x))
      (let ((y (x 1)))
        (func2 y)))
    в Racket можно (не обязательно, let тоже поддерживается) писать
    (define x 1)
    (set! x (func x))
    (define y (x 1))
    (func2 y)
    Таким образом, функция не страдает от излишней вложенности.
  4. В Racket в стандартной библиотеке есть функции для сопоставления по шаблону макросов (syntax-rules, syntax-case) и произвольных данных (match). Это также позволяет писать более компактный код без потери эффективности.

Семантические отличия:

  1. В Common Lisp одна среда для компиляции и выполнения. Это приводит разному поведению при загрузке исходников, компиляции и выполнении и при загрузке и выполнении скомпилированного файла. В первом случае будут доступны функции, определённые при компиляции. С другой стороны, если какая-то функция нужна только для раскрытия макроса, она всё-равно будет доступна в среде выполнения. Также перекомпиляция может дать существенно другой результат, по сравнению с компиляцией с пустого образа.
    В Racket есть понятие фаз. В каждой фазе доступны свои функции и можно указать какие модули подключать. Компиляция и выполнение происходят в разных фазах. Таким образом успешное выполнение гарантирует, что при (пере)компиляции и загрузке скомпилированного кода поведение останется неизменным.
  2. В Common Lisp макросы работают со списками, где идентификаторы представляются символами. В Racket с синтаксическими объектами. Синтаксический объект позволяет кроме символа хранить привязку (binding, синтаксическое значение) идентификатора. Это позволяет, во-первых, переименовывать идентификаторы при импорте, но сохранять макросы работоспособными, во-вторых, даёт возможность при раскрытии макроса сопоставить участки макроса и участки кода после раскрытия макроса, таким образом позволяя показывать место ошибки (оператор) в теле макроса. В-третьих синтаксические объекты позволяют автоматически реализовать «гигиену»: если идентификатор введён в теле макроса, то он не будет равен ни одному идентификатору в окружении использования макроса, если это не указано явно. В Common Lisp'е для этого можно использовать gensym, но, во-первых, это приходится делать явно, во-вторых, при переопределении flet или labels это не помогает, в-третьих, получается что все имена, использованные в теле макроса должны быть доступны в момент использования макроса, что нарушает инкапсуляцию.

Технологические:

  1. В Common Lisp нет продолжений (continuations). Есть их частичная замена: сигнальный протокол, но во многих случаях её недостаточно. Есть cl-cont, который тормозит.
  2. В Common Lisp соответственно нет меток фреймов (continuation-mark). Согласен, что нужны редко, но бывает.
  3. В SBCL финализатор (функция, выполняющаяся, когда сборщик мусора удаляет объект), повешенный на объект не может ссылаться на этот объект, так как сборщик мусора тогда считает, что ссылка на объект есть (из финализатора) и не собирает его. В Racket финализатору передаётся собираемый объект в качестве параметра. В целом ситуация с финализаторами, слабыми ссылками и хэшами по слабым ссылкам в свободных версиях Common Lisp достаточно печальна.

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

Common Lisp разрабатывался и используется в предположении, что пользователь программы — программист. Поэтому из языка намеренно исключены сложные для понимания конструкции (пользователь не обязательно квалифицированный программист), поэтому в языке мощнейший отладчик, позволяющий без остановки программы переопределять функции и вообще делать что угодно. Но из-за этого документация по большей части библиотек Common Lisp существует только в виде docstring и комментариев в коде (некоторые вообще считают, что код сам себе документация). Из-за этого обработка ошибок почти всегда оставляется на отладчик (главное сделать рестарт «перезапустить с последней итерации», а там пользователь сам разберётся). Из-за этого в программе проверяется только happy path (пользователь ведь «тоже программист»).

Racket разрабатывался и используется в предположении, что пользователь программы не программист, а задача разработчика написать программу так, чтобы она корректно работала при любых входных данных (если данные некорректны, то сообщала об этом в том месте, где данные были введены). Поэтому в языке эффективная библиотека для написания тестов, система контрактов на уровне модулей, макимально широкий спектр инструментов программирования (разработчик должен быть профессионалом!). Также реализована идея инкапсуляции: считается, что пользователь модуля не должен знать особенности реализации и, более того, не может в своём коде изменить функцию чужого модуля если это явно не разрешено разработчиком того модуля. Исходный код разумеется доступен, но его не требуется смотреть, чтобы использовать модуль. Достаточно документации. Поэтому реализована мощнейшая система документировния Scribble, а при реализации макроса есть возможность обеспечить указание на ошибки в коде, предоставленном макросу пользователем, не показывая потроха макроса.  

И поэтому в Racket нет CLOS (есть как минимум две реализации, но не используются) - провоцирует заплаточное программирование (monkey patching), поэтому отладчик намеренно ограничен (если ты отлаживаешь программу, значит ты не знаешь как она должна работать!), поэтому нет разработки в образе (image based) - она провоцирует разработку через отладку (а значит непонимание программы и проверку только happy path).

Таким образом, Racket и Common Lisp несмотря на внешнее сходство являются очень разными языками. И я рекомендую писать на Racket, если только конечными пользователями программы не являются исключительно программисты на Common Lisp.