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

Регистрация

Календарь

« Октябрь 2014  
Пн Вт Ср Чт Пт Сб Вс
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
 

Почему 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.

 

Почему Lisp


Здесь я хочу сравнить семейство Lisp'ов с остальными языками программирования.

Итак:

  1. Арифметика. В Lisp есть бесконечная целочисленная арифметика (то есть, например, произведение чисел от 1 до 1000 — обычное число в отличие от C++ и Java). Также реализована точная рациональная арифметика, что позволяет производить большую часть вычислений без потери точности совсем, а если участвуют иррациональные числа, то с заданной точностью. В Си аналогичная возможность доступна только с библиотекой типа GMP.
  2. Управление памятью. Во всех лиспах есть сборка мусора. Во многих также есть слабые указатели, которые позволяют создавать ассоциативные массивы хранящие данные только до тех пор, пока хоть что-нибудь ссылается на ключ.
  3. Динамичность. Переменная может хранить любое значение. Функции, классы, методы классов можно добавлять и изменять не прерывая выполнения программы.
  4. Скорость. В среднем в два-три раза медленнее Java, в десять раз быстрее, чем Python или Perl.
  5. Синтаксис. В Lisp все выражения имеют структуру (команда параметр1 параметр2 … параметрn). То есть открывающая скобка, команда с параметрами, разделёнными пробелами и закрывающая скобка. Это на первый взляд непривычно. (a+b+c)*(d+e) будет записываться как (* (+ a b c) (+ d e)). С другой стороны, влух мы говорим как в лиспе «произведение суммы a,b,c и суммы d и e».
  6. Макросы. Макрос — функция преобразующая свои параметры в кусок кода. У Lisp здесь два преимущества: синтаксис позволяет кусок кода и параметры представлять в виде списков, а языковая сред позволяет в макросе использовать все функции, доступные в Lisp (включая использование переменных). Это позволяет, например, сделать работу с ООП в виде библиотеки (синтаксис определения класса, методов, … описывается макросами). Также можно генерировать код по внешним условиям: например, класс для доступа к БД по структуре таблицы в той БД. В других языках аналогичные цели достигаются с помощью IDE, что не даёт нужной гибкости.
  7. ООП. В Common Lisp реализована идея обобщённых функций (CLOS). Классы определяются отдельно. Методы классов могут быть определены (или изменены) позднее. Более того, метод может принадлежать не одному классу, а нескольким (проверка класса идёт по всем аргументам метода). Аналогичная функциональность в C++ достигается только паттерном Visitor (для двух классов) или через ручной перебор классов в методе. Также есть возможность указать действие до-, после- или вокруг основного действия метода, что позволяет легко добавлять проверку входных данных и журналировние к уже существующим иерархиям классов. В остальных лиспах аналог CLOS реализуется через макросы. Также есть ООП библиотеки с другой семантикой (передача сообщений и класс = пространоство имен, например).
  8. Особые переменные или параметры. В Common Lisp есть возможность указать, что переменная «особая» (special). В этом случае есть возможность указать значение переменной для заданного блока программы (и всех функций, которые вызываются из этого блока). Таким образом сочетаются достоинства глобальных переменных (можно передавать значение в функцию не указывая его явно в параметрах) без их недостатков (особая переменная меняет значение только в блоке, при доступе из другого потока или любом выходе из блока, в том числе при исключительной ситуации, значение особой переменной восстанавливается, блоки могут быть вложенными). В Racket особые переменные называются параметрами и имеют синтаксис функции (но семантика та же).
  9. Обработка исключительных ситуаций. В Lisp'ах традиционно при обработке исключительной ситуации, как правило, есть возможность не только освободить ресурсы и вернуть значение, но вместо этого указать, что можно продолжить работы с другими условиями. Например, при ошибке при чтении таблицы из файла можно пропустить значение, пропустить строку, повторить чтение файла сначала, прервать чтение.
  10. Удобная работа с замыканиями. Замыкания — это функция (или несколько) со ссылками на переменные, в окружении которых она определена. Позволяет действительно инкапсулировать данные (область видимости у переменных только внутри заданной функции и всё). На самом деле аналогичная задача решается и другими средствами. Для функции в Си можно сделать переменные с параметром static. Для нескольких функции или для замыкания, возвращаемого из функции можно сделать класс и объект соответственно. У замыканий есть два преимущества: простота написания (никаких лишних сущностей наподобие имени класса) и то, что они являются функциями. То есть везде, где можно передать функцию, можно передать замыкание. С методом класса такой фокус не проходит.

Таким образом, очевидно, что Lisp имеет гораздо больше возможностей и позволяет писать гораздо более надёжные программы. Так почему же он не завоевал популярности? Есть несколько причин:

  1. Историческая (суеверная). На Лиспе писали систему искуственного интеллекта. Поставленная задача решена не была. Разработчики назвали язык одной из причин неудачи. После этого ни один руководитель не рисковал выбрать Лисп для своего проекта.
  2. Синтаксис. Он непохож на Бейсик. Он непривычен для новичков. Более того, он расширяем, что значит, что в каждом проекте синтаксис может быть слегка другим.
  3. Библиотеки. На самом деле, нет библиотек, потому что нет популярности, а из-за этого нет библиотек. Это не критичная проблема, так как все современные лиспы могут использовать библиотеки, написанные на Си.

Ну и до недавнего времени не было хороших бесплатных компиляторов. Сейчас есть SBCL (Common Lisp), Racket (диалект Scheme), ABCL (Common Lisp на Java VM).


 

Язык программирования Racket


Здесь будут мои статьи про язык программирования Racket.

Для начала, опишу, что это за язык и чем отличается от остальных. Racket — является диалектом языка Scheme, который в свою очередь относится к семейству языков Lisp. Его особенности: динамическая типизация (у значения есть тип, но у переменной — нет), сильная макросистема (во время компиляции можно делать почти любые действия), высокая производительность (программы работают медленнее, чем на Си или Си++, но быстрее, чем Java или Питон).

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

Ну и для примера пара программ на Racket.

Тривиальный Web-сервер:

#lang web-server/insta
(define (start request)
  (response/xexpr
   '(html
     (body "Hello World"))))

Простенький графический интерфейс:

#lang racket/gui ; A GUI guessing game
(define f (new frame% [label "Guess"]))
(define n (random 5))  (send f show #t)
(define ((check i) btn evt)
  (message-box "." (if (= i n) "Yes" "No")))
(for ([i (in-range 5)])
  (make-object button% (format "~a" i) f (check i)))