LINUX.ORG.RU

Анализ пользователей Common Lisp и Racket

 , ,


11

7

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

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

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

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

Взято с http://racket-lang.blog.ru/#post214726099

Хотелось бы знать, что по этому поводу думают пользователи ЛОРа. А также, мне кажется, что для Java и C++ будет где-то такая же разница.

★★★★★

Ответ на: комментарий от anonymous

Деление на ноль в первых двух предложениях

Почему же. Предполагается, что разработчик — квалифицироавнный (профессиональный) программист. Пользователь — неквалифицированный программист. Кстати, к UNIX это тоже относится. Сисадмин обязан быть «немножко программистом», иначе он не поймёт половину конфигов, которые являются программами

monk ★★★★★
() автор топика
Ответ на: комментарий от qweqwe

нормальные дженерики (как в CLOS)

Есть же классы. Зачем это нужно? Что в них там особенного?

multiple dispatch. Благодаря этой фиче 90% паттернов в CLOS становятся однострочниками.

Ну а :before и :after позволяют менять поведение чужих пакетов. В своём методе я могу к вызову метода написать свой враппер и вызывает его вместо метода, но с :before я могу например вывести все вызовы нужного мне метода в совсем чужой библиотеке. Правда мне ракетчики пару раз доказывали, что желание поломать потроха чужого модуля — иррационально с точки зрения надёжности программ, но иногда хочется «такой же, но с перламутровыми пуговицами» (с) и не хочется менять исходники чужого модуля.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Генерики для коллекций появились только в 6 версии, например. А еще у меня был какой-то баг неприятный, но я забыл какой.

anonymous
()
Ответ на: комментарий от anonymous

для Java и C++ будет где-то такая же разница.

Какая?

Заказчик программы на C++ хочет, чтобы его программа была лучшей в своём классе. Он согласен платить программистам за квалификацию и качество и согласен, что разработка ПО — скорее исследовательский проект.

Про java замечательно написано в первом комментарии этого треда:

Чувак, обычно программы которые ты видишь - это нечто, написанное индусами в течение последних лет 8 под веществами. Обычно когда такой код приходит тебе в руки, он даже не компилируется, не то чтобы нормально работать. Как работает этот код не знает никто, включая его создателей (они - в особенности). Тестов в типичном коде нет, и не предвидится (состояние кода и скорость, с которой заказчик хочет новые фичи, не позволит).

поэтому я использую java, чего и всем советую

Для заказчика программы на Java важно планирование бюджета и независимость результата от квалификации программистов.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

90% паттернов в CLOS становятся однострочниками.

Какие например?

Abstract factory, Flyweight : реализация initialize-instance

Factory method, Visitor, State, Strategy, : просто дженерик с multiple displatch

Singleton: метакласс

Command : замыкание

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Генерики для коллекций появились только в 6 версии

В CL вообще от sequence отнаследоавться нельзя, чтобы свою коллекцию сделать

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Учись читать до конца.

Ты писал на них что-нибудь большое?

Я вот самое большое писал одномерный физический движок. И соснул.

anonymous
()
Ответ на: комментарий от anonymous

Если бы дочитал, то прочитал бы, что я не помню.

anonymous
()
Ответ на: комментарий от monk

multiple dispatch. Благодаря этой фиче 90% паттернов в CLOS становятся однострочниками.

А нужны ли они вообще в языке с динамической типизацией и ФВП. Вон, стрэтэджи — точно просто многословная эмуляция частного случая применения ФВП. Визитор нужен лишь для купирования недостатков статики. Остальные не знаю, но думаю если и полезны, то от мультидиспатча им легче не станет. Можете привести пример какой-нибудь его незаменимости? По мне, так он просто в очень редких случаях позволяет описать задачу по-красивее.

Ну а :before и :after позволяют менять поведение чужих пакетов. В своём методе я могу к вызову метода написать свой враппер и вызывает его вместо метода, но с :before я могу например вывести все вызовы нужного мне метода в совсем чужой библиотеке.

Это разве не тоже самое, что super и inner call в классах?

qweqwe
()
Ответ на: комментарий от qweqwe

super и inner call в классах

Я же написал про чужие пакеты. super/inner должен написать разработчик класса, а навесить before/after может пользователь библиотеки.

Визитор нужен лишь для купирования недостатков статики

Нет. Визитор нужен, чтобы не городить typecase. Пример

(defmethod draw ((surface screen) (figure circle))
  ;; выводим круг на экран)

(defmethod draw ((surface printer) (figure circle))
  ;; выводим круг на принтер)

(defmethod draw ((surface screen) (figure rectangle))
  ;; выводим прямоугольник на экран)

(defmethod draw ((surface printer) (figure rectangle))
  ;; выводим прямоугольник на экран)

Можешь изобразить на racket/class как должна выглядеть функция (или метод) draw, чтобы можно было потом добавить пару классов к поверхностям (cairo, file, ...) и фигурам (triangle, pixel, ...) и не приходилось ломать базовые классы? Думаю, переизобретёшь Visitor. А для ситуации

(defmethod draw ((surface printer) (figure circle) (draw-type draft))
  ;; выводим прямоугольник на экран в упрощённом виде) 
даже Visitor не помогает. Приходится городить по методу на каждый класс на один из трёх параметров.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk
#lang racket
(require racket/match)

(define-struct circle ())
(define-struct rectangle ())
(define-struct screen ())
(define-struct printer ())

(define/match (draw surface figure)
  [((screen) (circle))
   (printf "draw: screen circle\n")]
  [((printer) (circle))
   (printf "draw: printer circle\n")]
  [((screen) (rectangle))
   (printf "draw: screen rectangle\n")]
  [((printer) (rectangle))
   (printf "draw: printer rectangle\n")])
x4DA ★★★★★
()
Ответ на: комментарий от x4DA

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

anonymous
()
Ответ на: комментарий от x4DA

Ну и еще то, что методы «открытые», то есть можно добавлять их на лету, не обязательно описать в одном месте.

anonymous
()
Ответ на: комментарий от x4DA

Там фишечка в том, что эти мультиметоды я могу размещать как рядом с объявлением структуры (обычное ООП)

class1
method1
method2

class2
method1
method2
Так и типа визитера
class1

class2

method1 for class1
method1 for class2

method2 for class1
method2 for class2

И все это делается одной конструкцией.

В ракете первый вариант делается через классы или генерики, второе через ветвление в процедуре.

anonymous
()
Ответ на: комментарий от x4DA

(define-struct circle ()) (define-struct rectangle ()) (define-struct screen ()) (define-struct printer ()) ...

А расширять как? Через (set! draw (let ((olddraw draw)) (lambda surface figure) (if (triangle? figure) (draw-triangle surface firgure) (olddraw surface figure)))))? А если надо отнаследовать и переопределить (в смысле define/override)?

Для одного параметра есть объекты и send. (send draw surface figure) правильно распределяет по поверхностям. И для новой поверхности достаточно сделать define/override. А вот для двух параметров — без CLOS гораздо сложнее.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

второе через ветвление в процедуре.

Классический Visitor работает не хуже, чем в C++. Просто multiple dispatch удобнее и нагляднее.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

А расширять как? Через (set! draw (let ((olddraw draw)) (lambda surface figure) (if (triangle? figure) (draw-triangle surface firgure) (olddraw surface figure)))))? А если надо отнаследовать и переопределить (в смысле define/override)?

Ну вообще можно хранить метаинформацию о форме матчинга и с каждым добавлением ее перегенерировать. И с наследованием все ок будет, вобщем-то.

anonymous
()
Ответ на: комментарий от anonymous

можно хранить метаинформацию о форме матчинга и с каждым добавлением ее перегенерировать

Это и будет multiple dispatch. Есть как минимум две реализации (swindle и gls). Но не приживается почему-то.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

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

Кстати, это, видимо, и есть причина, почему пользователи Racket не приемлют эту семантику. Там вообще во главе угла стоит идея, что как использовать модуль должен решать автор модуля, а не тот, кто этот модуль использует. В этом случае автор может учесть все возможные варианты использования и правильно их запрограммировать. Вплоть до того, что по-умолчанию все объекты, экспортируемые из модуля, неизменяемые. А для классов есть public, если это новый метод, override, если такой уже есть, кроме того public-final и override-final — значит, что дальше переопределять уже нельзя.

В Common Lisp ровно наоборот. В любой момент можно переколбасить любой класс или любого пакета. Защиты никакой: package::class позволяет менять даже неэкспортированные классы. Только пакет CL слегка защищен (предупреждение кидает: «вы уверены, что хотите изменить CL:LIST?». Скажешь да, и можешь убить всю систему).

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Кстати, это, видимо, и есть причина, почему пользователи Racket не приемлют эту семантику. Там вообще во главе угла стоит идея, что как использовать модуль должен решать автор модуля, а не тот, кто этот модуль использует.

Не, ну в генериках и ООП то все ок с этим.

anonymous
()
Ответ на: комментарий от anonymous

Не, ну в генериках и ООП то все ок с этим.

В Common Lisp?

Как мне объявить метод финальным (от его реализации, например, зависит инвариант)? Как сделать, чтобы новые методы можно было сделать только для новой операции (как в паттерне Visitor для примера на Java), а не частично переопределяя уже существующие?

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от anonymous

Хочу добавить ставшее уже классическим: в первом случае легко добавлять новые типы данных, а во втором - новые функции.

А в СИЛОСе похуй ваще.

anonymous
()
Ответ на: комментарий от x4DA

(define/match (draw surface figure)

На http://en.wikipedia.org/wiki/Visitor_pattern подробно описано, почему так нельзя:

A naive way to solve this would be to maintain separate functions for each file format. Such a save function would take a drawing as input, traverse it and encode into that specific file format. But if you do this for several different formats, you soon begin to see lots of duplication between the functions. For example, saving a circle shape in a raster format requires very similar code no matter what specific raster form is used, and is different from other primitive shapes; the case for other primitive shapes like lines and polygons is similar. The code therefore becomes a large outer loop traversing through the objects, with a large decision tree inside the loop querying the type of the object. Another problem with this approach is that it is very easy to miss a shape in one or more savers, or a new primitive shape is introduced but the save routine is implemented only for one file type and not others, leading to code extension and maintenance problems.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Another problem with this approach is that it is very easy to miss a shape in one or more savers, or a new primitive shape is introduced but the save routine is implemented only for one file type and not others, leading to code extension and maintenance problems.

А с визитером сразу все заметишь, потому что изменишь интерфейс, и ИДЕ покажет где нужно добавить метод, потому что статическая типизация, wait, oh shi~

anonymous
()
Ответ на: комментарий от anonymous

А с визитером сразу все заметишь

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

При чём тут статическая типизация?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

при попытке описать класс без этого метода получишь ошибку компиляции.

сорт статической типизации

anonymous
()
Ответ на: комментарий от anonymous

сорт статической типизации

Я всё-таки неправ. Ошибка будет ошибкой выполнения при выполнении макроса (class ...). Но отловится на первом же тесте.

А в целом, необязательная статическая типизация (в стиле declare в Common Lisp или контрактов в Racket) — очень полезная штука. В том же SBCL позволяет хорошо оптимизировать код и избегать ошибок по невнимательности. Также автоматически получаешь проверку параметров на корректность.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А в хаскелле я получу жалобу на нетотальность ветвления

Хаскелль красивый язык, но как там писать с больше двух монад в контексте я так и не понял (например, есть IO, X, Maybe, ...) — сильно замороченно.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Нужно осилить трансформеры монад. Это следующий этап, я его прошел, но дальше не двинулся пока. Это не так уж сложно. Лучше всего спрашивать совета. У меня в скайпе был советчик. Но можно и здесь, и на двачах.

anonymous
()
Ответ на: комментарий от monk

с больше двух

Прочитал как больше одной:) Нужно выбрать, какая будет главная.

anonymous
()
Ответ на: комментарий от anonymous

Но можно и здесь, и на двачах.

Ну вот пример. Как сделать функцию, чтобы она читала из IO строку, если строка = «ok», то выполняет Refresh (http://xmonad.org/xmonad-docs/xmonad/src/XMonad-Operations.html#refresh) и возвращает Just «ok», иначе возвращает Nothing? Я даже тип этой функции не могу представить :-(

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Она не будет возвращать Just «ok», она уже зашкварена ИО. У тебя может получиться Io (Maybe String) или MaybeT IO String

anonymous
()
Ответ на: комментарий от anonymous

Если подумать дальше, то X в refresh - это StateT IO Значит, если применить к нему аккуратно лифтануть сначала runState или как-то так, то получится как раз IO smth. А дальше лифтануть твою функцию String -> Maybe String и все будет збс. Ну может альтернативку дописать.

anonymous
()
Ответ на: комментарий от anonymous

может получиться Io (Maybe String) или MaybeT IO String

Я это и имею в виду. Из за Refresh в одной из веток ещё X должен в типе быть где-то. И на этом у меня фантазия кончается. А это ведь ещё не самый страшный случай, монад-то много... (и все они могут оказаться в одной функции 8-0)

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

монад-то много

Из большинства можно экстрагировать значение.

anonymous
()
Ответ на: комментарий от anonymous

аккуратно лифтануть сначала runState ... получится как раз IO smth

Так она ведь в монаде X должна действие делать? Или IO её заменит?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В Common Lisp?

В ракетке. Так же как методы в clos инстансы генериков и реализации интерфейса можно определять где угодно.

anonymous
()
Ответ на: комментарий от monk

Я всё-таки неправ. Ошибка будет ошибкой выполнения при выполнении макроса (class ...). Но отловится на первом же тесте.

Даже не на тесте а на запуске файлы, скорее всего, т.к. class-формы обычно не внутри ф-й вызывают и исполнятся сразу при запуске программы.

anonymous
()
Ответ на: комментарий от anonymous

Нужно осилить трансформеры монад. Это следующий этап, я его прошел, но дальше не двинулся пока. Это не так уж сложно.

Не сложно. Но уже просто с монадами код превращается в малочитабельное говно. С трансформерами это полный ридонли хаскиарт.

anonymous
()
Ответ на: комментарий от monk

Монада X это на самом деле StateT IO

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.