Подскажите пожалуйста, как в CL правильно реализовать следующую задачу.
Эта задача - из категории графики, я хочу определить интерфейс взаимодействия с графическими объектами. И нет, это не GUI, просто графика, хотя для GUI решение должно быть точно таким же. Я хочу задать узловое взаимодействие объектов в стиле node tree блендера или деревьев Houdini или может быть даже сетей GEGL, но с последним я не уверен. Короче, просто граф узлов, где узлы - это функции, на которые я и хочу накладывать ограничения.
Я хочу задать интерфейс взаимодействия программы (скорее в данном случае "фрэймворка", а не программы). В терминах С++ и прочих подобных языков это называется абстрактным интерфейсом. Например в Swift это сделано очень красиво в терминах языка: в определении класса указывается кроме родителя еще список интерфейсов, которые класс реализует. Кратко, понятно, однозначно. Заранее оговорюсь, что ООП как цель меня не интересует. Любой механизм подойдет. Просто я знаю как именно это делается в ООП и поэтому привожу пример оттуда.
Основное требование к решению - чтобы оно было понятно человеку, т.е. было бы документацией к самому себе.
К слову, использования CLOS я бы хотел избежать просто из-за его сложности (= высокого входного порога для новичка). Да и производительность вызывает вопросы (но если я заблуждаюсь относительно производительности, поправьте меня).
Если с CLOS я неправ и он достаточно прост в необходимом подмножестве, было бы интересно узнать.
> Просто я знаю как именно это делается в ООПCommon Lisp это ООП-язык.
> Если с CLOS я неправ и он достаточно прост в необходимом подмножестве, было бы интересно узнать.Да он просто прост. Относительно сложен MOP, но он вообще говоря и не является частью CLOS.
> В терминах С++ и прочих подобных языков это называется абстрактным интерфейсомВ CL тебе надо определить несколько generic с помощью defgeneric, читай
http://lisper.ru/pcl/object-reorientation-generic-functions.
А потом реализовывать эти методы для своих классов.
Вижу два решения: динамическое и статическое.
Динамическое - пакет с экспортированными generic-функциями.
И статическое - пакет с макросами которым надо сообщать тип объекта и
по ним они должны найти в хеш-таблице (а лучше в дереве)
соответствующую функцию и генерировать код с вызовом этой функции.
Динамический метод более удобен, прост и согласуется с динамической
природой CLOS. Динамичность нужно оплачивать зачастую лишними
инструкциями поиска методов и сборки общего метода.
Статический подобен пути C++, Java, etc. Минусы - тупая лямбда
из дерева или очень сложная система сборки этой лямбды. Также
требования к стабильности этого дерева лямбд, что довольно чуждо
динамической природе лиспа. Также надо постоянно указывать тип.
Со вторым методом лисперы часто не парятся и просто пишут:
(defun draw-fucking-picture (picture)
...)
Но это с твоими интерфейсами плохо согласуется.
ИМХО лучше брать CLOS потому что удобнее и проигрыш в
производительности зачастую преувеличивается. Если идея годная, то её
можно оптимизировать потом. Помни: преждевременная оптимизация -
корень всех зол. Призводительный SBCL создавали не для того чтобы
мудохаться с этой фигнёй.
Из коробки этого в CL нет.
В чём проблема? В процессе сборки.
В С++ или Яве тебе известен момент, когда сборка программы закончена. Если на этот момент какой-то из декларированных интерфейсов не реализован, то можно выдать ошибку компоновки.
В CL нет такой встроенной возможности. Сборка программы закончена в безконечно далёком будущем. Если интерфейс на данную секунду не реализован, то это не означает проблемы.
В этом есть минусы (программа может уйти в продакшен недописанной) и плюсы (её можно всегда подправить).
Я бы, если бы хотел такого, то ввёл бы понятие "интерфейс". Интерфейс - это просто некий класс для которого определён метод "перечисли-мои-методы-с-сигнатурами". Это можно написать достаточно
красиво.
Например (точный синтаксис defclass не помню, так что примено):
(defclass interface2 (interface1))
(defmethod перечисли-мои-методы-с-сигнатурами ((x interface2))
(append '((перевернуть (ось))) (call-next-method)))(defclass квадратик (фигура interface2))Далее, наследование от интерфейсов, равно как и наследование интерфейсов друг от друга - можно сделать через наследование CLOC классов. CLOS позволяет множественное наследование.
Вряд ли можно как-то более просто - тебе придётся писать тогда свою объектную систему. Я не знаю других популярных объектных систем кроме CLOS, хотя мне CLOS не слишком нравится.
Тебе будет проще понять CLOS, чем возиться с альтернативной системой, которая наверняка будет недоделанной.
Ну и останется сделать проверку, что всё определено. Т.е. найти, от кого наследует квадрат, вызвать для каждого интерфейса "перечисли-мои-методы-с-сигнатурами" и проверить, что код определён.
Это уже через метаклассы (см. closer-mop или swank).
Этот путь истинно лисповый - всё динамично. Если нужна производительность, то можно как сделать?
Например, через систему именования. Определять метод как ИмяКласса--ИмяМетода. Туповато, зато быстро будет работать. Для виртуальных вызовов всё равно
придётся использовать что-то навроде таблицы виртуальных методов. Короче, это путь реализации собственной объектной системы. Это не так уж сложно, но это работа.
Проверять полноту воплощения интерфейса всё равно получится только динамически. Ну и с переопределением будет не так-то просто: готовься к многократным перезапускам
своей системы или к тому, что придётся писать функции очистки пространств имён.
Я понял идею с дженериками, как и статическими функциями и оба метода мне не нравятся.
В случае с дженериками проблема в том, что в С++-подобных языках синтаксис языка является ограничителем и таким образом, помогает накладывать ограничение на тип. Как накладывает ограничение дженерик, я понять не могу.
"Статический" вариант печален тем, что это "закат солнца вручную". Мало того, что его надо написать, отладить и прочее, так его ещё ж придётся потом понимать тем, кто будет этим пользоваться. Пугающий вариант, другими словами.
Что касается последнего комментария с примером дефкласса: я не совсем понял, точнее я надеюсь что я не понял, но ближайшее похожее, что приходит мне на ум, это реализация GTK+. Например вот: https://git.gnome.org/browse/gimp/tree/app/core/gimplayer.h
(аналогия в том, что реализация RTTI полностью сделана руками с поисками-переборами и т.п. Кажется, получается "статический" вариант).
У меня же вопрос в том, как наложить ограничение на тип (класс) так, чтобы программист мог сразу видеть это ограничение и использовать его как guideline?
P.S. Нашел http://habrahabr.ru/post/230619/ и курю теперь эту тему. Никогда не приходило в голову разбираться с MOP, но похоже, время пришло...
Если ограничение состоит в том, чтобы убедиться, что интерфейс должен быть полностью реализован (чтобы всего хватало), то это делается по мотивам моего примера. Нужно ещё две функции: "
проверить-определённость-всех-интерфейсов (имя-класса)" и "
проверить-определённость-всех-интерфейсов-всех-классов". Они пишутся на основе "
перечисли-мои-методы-с-сигнатурами" и проверят, что для данного класса существует каждый из методов, которые возвращает эта функция. Для этого нужны примитивы "список предков класса" и "список всех классов". Но они безусловно есть либо в стандарте, либо в MOP. Лучше всего смотри сначала на swank - инспектор же должен показывать предков класса.
В этом случае программисту вполне понятно: смотришь, какие "интерфейсы" наследует твой класс и смотришь на методы "
перечисли-мои-методы-с-сигнатурами" каждого из этих интерфейсов. Т.е. это прямая калька с C++, не считая того, что методы интерфейса у тебя прописаны в теле функции "
перечисли-мои-методы-с-сигнатурами".
Если нужно что-то другое, то разъясни характер ограничений.
Статический вариант может оказаться предпочтительным и он вовсе не так уж сложен в реализации. Например, я для себя подобное сделал и пользуюсь в своей библиотечке
https://bitbucket.org/budden/budden-tools . Предпочтителен он своей компактностью. С него всегда можно перейти на более мощный CLOS. А вот когда тебе потребуется перейти с CLOS на что-то более быстрое, то может постичь облом.
> С++-подобных языках синтаксис языка является ограничителем и таким образом, помогает накладывать ограничение на тип.
Об этом много писали в старых книгах по С++, как будто в этом есть большой смысл. В динамических языках на этом не заморачиваются.
> У меня же вопрос в том, как наложить ограничение на тип (класс) так, чтобы
> программист мог сразу видеть это ограничение и использовать его как guideline?
Написать комментарий?
Кстати, вас только решение на CL интересует? А то ведь можно воспользоваться стандартными средствами Racket (у него по дефолту больше двух видов объектных систем)
Прочитал статью Лавсана на хабре. Увы, она мне в голову не влазит. Слишком велик пробел в моих знаниях, сложно за что-то зацепиться.То есть, в общем идея ясна, но детали реализации опираются на контекст, о котором я ни сном, ни духом.
@den73: Спасибо за комментарий, я примерно понимаю идею "прибивания списка методов гвоздями", как это и сделано в С++&Cº. И кстати, в budden-tools в каком файле смотреть?
Если не затруднит, дайте ссылки на примеры реализаций разных вариантов.
@m4: Я с Racket вообще никогда не сталкивался.
@archimag: Это не инструмент, он ничего не автоматизирует.
В моём примере много других наворотов, из них CLOS ещё не самый сложный.
https://bitbucket.org/budden/budden-tools/src/default/variable-type.lisp - здесь ищи "defmacro |^|"
Замысел состоит в том, чтобы всегда писать
(^ объект поле), а то и
объект^поле - но это уже следующий
уровень наворотов ридера и тебе в это лучше пока не лезть. Пока что ^ - это обычный символ из пакет :budden-tools.
Итак, если в коде написано
(^ объект поле), то для начала оно макрорасширяется в carat-implementation, а затем в
(common-carat-implementation объект поле)common-carat-implementation определена дважды - в виде
macro и
compiler-macroЕсли срабатывает макрос (например, при интерпретации), то без вопросов подставляется функция
runtime^Эта функция делает динамическую диспетчеризацию при условии, что класс объекта будет известен только в рантайме.
Сильно упрощая, можно сказать, что она в рантайме берёт
class-of объекта и ищет функцию с именем
класс-поле и выполняет её.
Если же мы идём по ветке
compiler-macro, что в норме бывает в ходе компиляции, то
(define-compiler-macro common-carat-implementation)пытается в окружении компиляции найти информацию о декларации типа для объекта. Например, если ты написал
(declare (type класс1 объект))
(^ объект поле)то есть хорошие шансы, что ещё на этапе компиляции ^ будет расширено в
(класс1-поле объект)
Продолжение следует - надо отойти от компа.
Это я описал минимальный и простейший вариант. Механизм там расширяется в несколько направлений, поэтому код выглядит гораздо сложнее, чем данное описание.
Можно задавать правила преобразования ^ в имя функции для разных классов и для конкретного биндинга - это всё возникло из требований практики.
Нужно учесть, что доступ к информации о типах переменных во время компиляции в стандарте не прописан, поэтому
функция variable-type-or-class зависит от реализации лиспа и сейчас она существует только для SBCL и Lispworks.
С идейной точки зрения это скорее синтаксический сахар для избежания многократного повторения имени структуры в выражениях типа
(структура-поле объект). Как реализация ООП он довольно хрупок из-за того, что не всегда очевидна связь между декларациями типа и реальным
типом, который будет доступен при компиляции. Я уже точно не помню, в чём именно там были трудности, но они иногда были.
Однако, ^ всё же является своеобразной (и весьма неэффективной) реализацией полиморфизма. Для моих задач этого хватало.
К этому можно добавить, что у структур есть одиночное наследование и для них можно определять родовые функции. Так получаем
легковесный "CLOS без классов для неосиляторов". Хотя вся сложность, связанная с родовыми функциями, никуда не девается, а динамическое
переопределение структурного типа, вообще говоря, невозможно (в частных случаях возможно).
А вот тебе для разгона про MOP
>
(ql:quickload :closer-mop)
>
(defclass parent () ())
#<standard-class parent>
> (defclass child (parent) ())
#<standard-class child>
> (closer-mop:class-direct-superclasses (find-class 'child))
(#<standard-class parent>)
Не так страшно и читать ничего особо не надо.
Далее пишешь closer-mop: в SLIME и нажимаешь Tab - вот и весь интерфейс. Наверняка быстро найдёшь то, что тебе нужно.
Если тебя смутило это
(defmethod перечисли-мои-методы-с-сигнатурами ((x interface2))
(append '((перевернуть (ось))) (call-next-method)))То элементарно пишется макрос, который это автоматизирует. Кстати, я неправильно написал, метод должен оперировать не с экземплярами, а с самими классами. Примерно как-то так:
Так будет более правильно
(defmethod перечисли-мои-методы-с-сигнатурами ((x (eql (find-class 'parent)))) (append '((перевернуть (ось))) (call-next-method))И тебя не должно смущать, что это "делается руками", потому что это всё легко оборачивается макрос .Лисп потому и не С, что в нём гораздо более мощный "препроцессор".
Вот тебе полный пример.
(in-package :cl-user)(eval-when (:compile-toplevel)
(error "Загружай этот файл с помощью load. Чтобы компилировать, макросы должны быть в отдельном файле, к-рый загружается сначала"))(format t "~%Example of declarative interfaces~%")(defmacro def-root-interface (name methods)
`(progn
(defclass ,name () ())
(defmethod list-of-interface-method-names ((x (eql (find-class ',name))))
(copy-tree ',methods))))(defmacro def-child-interface (name parent childs-methods)
`(progn
(defclass ,name (,parent) ())
(defmethod list-of-interface-method-names ((x (eql (find-class ',name))))
(append (copy-tree ',childs-methods)
(list-of-interface-method-names (find-class ',parent))))))(defmethod list-of-interface-method-names (x) nil) ; зло, но от него не увернуться
(defmacro show-expr (expr &optional (stream '*trace-output*))
"Shows expression and its value on the trace-output"
(let ((e1 expr))
(alexandria:once-only (e1)
`(progn
(format ,stream "~S = ~S~%" ',expr ,e1)
,e1))))(defun total-list-of-interface-method-names (class)
"Это надо применять для обычных классов, не для интерфейсов"
(let ((result nil))
(dolist (parent (closer-mop:class-direct-superclasses class))
(setf result (append result (list-of-interface-method-names parent))))
result))(defun check-all-interface-methods (class)
(let ((result))
(dolist (method-name (total-list-of-interface-method-names class))
(push `(,method-name
,(or (ignore-errors
(find-method
(coerce method-name 'function)
'()
(list class)
))
"INTERFACE METHOD IS UNDECLARED!"))
result))
(nreverse result))); определение несколкьих интерфейсов
(def-root-interface figure (m1 m2))(def-child-interface square figure (a set-a))(show-expr (list-of-interface-method-names (find-class 'square)))(def-root-interface square2 (a set-a))(defclass generic-figure (figure) ())(defclass real-square (generic-figure square2) ())
(defmethod set-a ((x real-square)) (print "set-a invoked"))(show-expr (total-list-of-interface-method-names (find-class 'real-square)))(show-expr (check-all-interface-methods (find-class 'real-square)))Всё зачеркнуть. Более правильно вот так, хотя тоже неправильно.
Неправильность состоит в том, что не находится возможность вызвать m1 из предка, поэтому
в отчёте пишется, что m1 не определена для real-square. Я не знаю, как это исправить в общем случае.
В частном нужно пошарить по всем методам данной родовой функции, по дереву предков данного класса и понять, можем ли мы
что-то вызвать. Это может оказаться нелегко. Или я что-то не догнал - я вовсе не специалист в MOP и редко им пользуюсь.
(in-package :cl-user)(eval-when (:compile-toplevel)
(error "Загружай этот файл с помощью load. Чтобы компилировать, макросы должны быть в отдельном файле, к-рый загружается сначала"))(format t "~%Example of declarative interfaces~%")(defmacro def-root-interface (name methods)
`(progn
(defclass ,name () ())
(defmethod list-of-interface-method-names ((x (eql (find-class ',name))))
(copy-tree ',methods))))(defmacro def-child-interface (name parent childs-methods)
`(progn
(defclass ,name (,parent) ())
(defmethod list-of-interface-method-names ((x (eql (find-class ',name))))
(append (copy-tree ',childs-methods)
(list-of-interface-method-names (find-class ',parent))))))(defmethod list-of-interface-method-names (x) nil) ; зло, но от него не увернуться
(defmacro show-expr (expr &optional (stream '*trace-output*))
"Shows expression and its value on the trace-output"
(let ((e1 expr))
(alexandria:once-only (e1)
`(progn
(format ,stream "~S = ~S~%" ',expr ,e1)
,e1))))(defun list-of-predecessor-interface-method-names (class)
(let ((result nil))
(dolist (parent (closer-mop:class-direct-superclasses class))
(setf result
(append
result
(list-of-interface-method-names parent)
(list-of-predecessor-interface-method-names parent)
)))
result))(defun check-all-interface-methods (class)
"Это мы вызываем для обычного класса, а не для интерфейса"
(let ((result))
(dolist (method-name (list-of-predecessor-interface-method-names class))
(push `(,method-name
,(or (ignore-errors
(find-method
(coerce method-name 'function)
'()
(list class)
))
"INTERFACE METHOD IS UNDECLARED!"))
result))
(nreverse result))); определение несколкьих интерфейсов
(def-root-interface figure-i (m1 m2))(def-child-interface square-i figure-i (a set-a))(show-expr (list-of-interface-method-names (find-class 'square-i)))(def-root-interface square2-i (a set-a))(defclass real-figure (figure-i) ())
(defmethod m1 ((x real-figure)) (print "m1 invoked"))(defclass real-square (real-figure square2-i) ())
(defmethod set-a ((x real-square)) (print "set-a invoked")); запрос к метаданным - какие методы должны быть определены для класса
(show-expr (list-of-predecessor-interface-method-names (find-class 'real-square)))(show-expr (check-all-interface-methods (find-class 'real-square)))Жесть какая...
Я правильно понимаю, что все эти сложности ради динамического определения (resolve) имен функций или полей классов?
Я тут подумал и решил, что поскольку мне динамизм не нужен (ну, или я думаю, что он мне не нужен), поэтому самым чётким решением будут макросы, поскольку все резолвы должны случиться в compile-time. И именно ошибки макроподстановок - тот инструмент, на который можно опереться. Именно тут компилятор проверяет соответствие типов функциям за меня.
В целом меня не покидает ощущение, что я смотрю некоторый фильм с середины. Или другими словами, я не могу понять причин происходящего. Возвращаясь всё к тому же многострадальному С++, который я изучал очень давно подростком, зная только паскаль и ассемблер, мне было понятно, что и как делается и даже иногда "почему" (в этом я доверял Страуструпу на слово, причем местами зря). В данной же теме, несмотря на прочитанные книги, sicp и прочее, я не понимаю, ну почему это настолько сложно? Моё основное предположение состоит в том, что я не знаю некоторых начальных вещей относительно clos или mop.
Что именно жесть? ^ из библиотеки или этот пример?
Если хочешь понять по аналогии, можешь вот это попробовать почитать, тут на основе Дельфи и SQL.
http://rosinmn.ru/ecovillage/lisp/lisp-tutorial.htm#_Toc346479881
> я не понимаю, ну почему это настолько сложно?
1) den73 известный "извращенец"
2) который пытается решить задачу, которую решать не надо
3) ибо в динамический языках попытка контроля подобных ограничений на стадии компиляции просто неуместна
Ты неправ, неправ и ещё раз неправ. Изучи Transact-SQL.
@den73: Спасибо за ссылку. Я просмотрел наискосок всё и повнимательнее конкретно "Полиморфные (родовые) функции, структуры и классы" и немного далее.
Есть стойкое ощущение изложения в стиле "поток сознания". Например, вот фраза прямо из соседнего параграфа: "У методов может быть квалификатор. Основной метод – это метод, у которого нет квалификатора." Определение "квалификатора" дано далее(!) по тексту и найдено с помощью Ctrl+F.
Или вот сразу фраза "...(overload) функции, которые исполняются по-разному в зависимости от типов нескольких аргументов". Это называется сигнатурой.
Но это всё мелочи, просто мне было важно уяснить уровень документа.
@archimag:
> в динамический языках попытка контроля подобных ограничений на стадии компиляции просто неуместна
Поясни, каким инструментом и на каком этапе решается задача, которую я озвучил в самом начале? Я имею в виду в самом начале упомянутый абстрактный интерфейс или аналогичную ему функциональность.
Мне нужно, чтобы компьютер делал максимум работы за меня. Чтобы не я, опираясь на комментарии, мысленным усилием гарантировал корректность модели, а (в данном случае) компилятор брал на себя как минимум часть этой работы.
> Я имею в виду в самом начале упомянутый абстрактный интерфейс или аналогичную ему функциональность.
Тебе надо определить набор generic-методов и специализировать эти методы для своих классов. Скажем, MOP или gray streams являются демонстрациями такого подхода. Который предоставляет все необходимые возможности для проектирования и разработки любых систем. ИМХО, CLOS мощнее и удобнее, чем объектная система Java или C++.
> Мне нужно, чтобы компьютер делал максимум работы за меня.
О какой работе идёт речь? Некоторые адепты статической проверки типов утверждают, что строгий контроль типов на этапе компиляции позволяет сократить время разработки. Однако, на практике разработка на языках с динамической типизацией обычно позволяет получать работающий код значительно быстрее. Так что рассуждения о том, что компилятор что-то там делает за тебя просто не соответствуют действительности. Правда, языки со статическим контролем типов обычно выполняются быстрее, поскольку компилятор обладает точной информацией о типах и может генерировать более оптимальный код. Из чего можно заключить, что проектирование статически контролируемых интерфейсов/ограничений/etc замедляет разработку, но ускоряет результирующий код.
pva - документ был полезен или нет? Не надо было читать всё, я дал ссылку на конкретный пункт оглавления.
Архимаг, ты гонишь стандартный религиозный бред. Сам подумать хоть раз пробовал? В том же CL при компиляции файла проверяется существование всех функций и переменных, на которые ссылается код в файле. При отсутствии функции или несоответствии параметров выдаётся предупреждение. Компилятор таки делает это за тебя. Ты гордо отворачиваешься при виде таких предупреждений или ты ими пользуешься?
> Из чего можно заключить, что проектирование статически контролируемых
интерфейсов/ограничений/etc замедляет разработку, но ускоряет
результирующий код.
И снижает вероятность его падения во время работы ровно на то количество проблем, которое было выявлено на этапе компиляции. К тебе пришёл человек к нормальным вопросом, а ты ему пытаешься мозг отформатировать под секту динамического программирования. Если инструмент X даёт сервис и делает за человека работу, которой лисп не делает, а все лисперы оказываются сектантами - человек просто уйдёт на инструмент X. Секта оправдана, когда требуемое действительно не нужно или хотя бы невыполнимо. В данном случае оно нужно и легко выполнимо.
> CLOS мощнее и удобнее, чем Что ты называешь "мощью"? И относительно "удобнее" - о вкусах не спорят.
> Так что рассуждения о том, что компилятор что-то там делает за тебя просто не соответствуют действительности
Так и хочется сказать "а мужики-то не знают!". Или, другими словами, посмотри на индустрию. Как можно игнорировать Java?
Только не надо приводить штампы про тупых менеджеров или наличия/отсутствия программистов на Java или CL. Конкуренция давно бы доказала, что джава (или вообще разработка с использованием статической проверки типов) нежизнеспособна, если бы это было так.
Кстати, вот ещё вопрос в тему: а как компилятор помогает проверять корректность программы? Каким образом вообще определяется и задаётся корректность в CL? Вот в статических языках, я слышал, для этого используются системы типов. И, кстати, именно оттуда растут ноги моего первого вопроса, потому что именно соответствие абстрактному интерфейсу/протоколу гарантирует мне соответствие кода некоторым ожиданиям. Я правильно понимаю, что ты предлагаешь использовать для подобных целей комментарии? Если да, то как вообще проверять корректность кода? Я понимаю, что легко писать в условиях неограниченного роста технического долга, но как с этим долгом разбираться в конечном итоге? Вот это интересный вопрос.
> CLOS мощнее и удобнее, чем Что ты называешь "мощью"? И относительно "удобнее" - о вкусах не спорят.
> Так что рассуждения о том, что компилятор что-то там делает за тебя просто не соответствуют действительности
Так и хочется сказать "а мужики-то не знают!". Или, другими словами, посмотри на индустрию. Как можно игнорировать Java?
Только не надо приводить штампы про тупых менеджеров или наличия/отсутствия программистов на Java или CL. Конкуренция давно бы доказала, что джава (или вообще разработка с использованием статической проверки типов) нежизнеспособна, если бы это было так.
Кстати, вот ещё вопрос в тему: а как компилятор помогает проверять корректность программы? Каким образом вообще определяется и задаётся корректность в CL? Вот в статических языках, я слышал, для этого используются системы типов. И, кстати, именно оттуда растут ноги моего первого вопроса, потому что именно соответствие абстрактному интерфейсу/протоколу гарантирует мне соответствие кода некоторым ожиданиям. Я правильно понимаю, что ты предлагаешь использовать для подобных целей комментарии? Если да, то как вообще проверять корректность кода? Я понимаю, что легко писать в условиях неограниченного роста технического долга, но как с этим долгом разбираться в конечном итоге? Вот это интересный вопрос.
О, дублирование моего комментария (а что, браузер ничего не сделал, я и ткнул кнопку ещё раз, думал что я промазал) демонстрирует замечательный пример быстрой разработки. Как в нём реализована идемпотентность? Я думаю, я знаю как. Надо поискать комментарий...
s/Как в нём реализована идемпотентность?/Как в коде сайта реализована идемпотентность?/
@den73: да, был полезен. Я ж в самом начале поблагодарил! Просто прокомментировал, что выглядит потоком сознания. (Признаться, возникло ощущение, что местами писал я сам (: )
Фрагмент отсюда https://groups.google.com/forum/#!topic/comp.lang.lisp/cLRrER_KIgo :
>the real question here is "why do you think you need an abstract class
>in common lisp, and what do you think it would get you?"It's interesting to note that Flavors, one of the two main influences on
the design of CLOS, *did* have the notion of abstract classes. DEFFLAVOR
had an :ABSTRACT-FLAVOR option that indicated that the flavor couldn't be
instantiated by itself. It also had :REQUIRED-INSTANCE-VARIABLES and
:REQUIRED-METHODS, to declare that a flavor incorporating this flavor as a
base has to define these instance variables (what CLOS calls slots) and
methods, since methods in the base flavor will reference them
(:REQUIRED-METHODS is analogous to C++'s technique where the base class
declares a method with '= 0'). These things allow the base class to define
a protocol that mixins are supposed to implement.
и далее Phil Stubblefield пишет (текст выделен мною):
f you have the free time, then by all means take the
time to define DEFINE-ABSTRACT-METHOD and DEFINE-ABSTRACT-CLASS,
expanding their capabilities as necessary to meet your needs. You'll
probably learn some invaluable lessons about macros and about CLOS.
Then, after you've defined the macros to your satisfaction -- consider
throwing most of your work away!
Why would you do that? Well, before maintaining or enhancing your
code, the next programmer who comes along must first understand it.
If your code uses your own high-level abstractions, then such a
programmer must digest those definitions *before* even beginning to
understand your domain-specific code.
> Или, другими словами, посмотри на индустрию. Как можно игнорировать Java?
Зачем её игнорировать? Но как можно игнорировать Python/Perl/Ruby/Javascript? Это всё существует параллельно.
> Каким образом вообще определяется и задаётся корректность в CL?
Если программа работает корректно, то она корректна, LOL!
Я пытаюсь донести простую мысль, что в данной области есть два конкурирующих подхода и оба широко представлены в индустрии: статическая и динамическая типизация. Если тебя интересует максимальные гарантии на этапе компиляции, то лучше посмотри в сторону Haskell. Динамические же языки предлагают совершенно другой подход, основанный на лёгкости изменения кода (в том числе, уже работающего) и быстром его запуске для проверки результата изменений (REPL).
Я не говорю, что динамическая типизация лучше, чем статическая (я пишу как тех, так и на других языках), я только говорю, что CL это динамический язык и ты не можешь эффективно использовать его в статическом стиле.
> К тебе пришёл человек к нормальным вопросом, а ты ему пытаешься мозг отформатировать под секту динамического программирования.
LOL, повторюсь, я только объясняю, что CL это динамический язык. И это не просто случайное недоразумение, а вполне осознанное решение людей, которые его когда-то создавали. Если по каким-то причинам это не устраивает, то нужно просто взять другой язык.
Как уже говорил motopeh, тебе нужно использовать макросы. Думаю, что интерфейс взаимодействия программы будет задаваться несколькими макросами. Она же будут выполнять проверку наличия реализации интерфейса (на этапе компиляции). Например, код по определению интерфейсов и реализации их в классах может выглядеть примерно так:
(define-interface my-interface ()
(:some-method (object (arg-1 integer) (arg-2 integer)))
(:some-method (object (arg-1 double) (arg-2 double))))(define-interface other-interface ()
(:other-method (object &rest args)))(define-class my-class ((:implement my-interface))
(:property my-slot :reader other-slot-of)
(:some-method ((object my-class) (arg-1 integer) (arg-2 integer))
(make-some-with-integer (my-slot-of object) arg-1 arg-2))
(:some-method ((object my-class) (arg-1 double) (arg-2 double))
(make-some-with-integer (my-slot-of object) arg-1 arg-2)))(define-class other-class ((:extends my-class)
(:implement other-interface))
(:property other-slot :reader other-slot-of)
(:other-method ((object other-class) &rest args)
(make-some-with-list (other-slot-of object) args)))Если сущностей больше, то нужно определить макрос для каждой.
Случайно наткнулся на https://github.com/fare/lisp-interface-library/ , судя по описанию ( https://common-lisp.net/~frideau/lil-ilc2012/lil-ilc2012.html ), вроде как должна подойти. Все-таки у racket интерфейсы как-то более привычно выглядят, но как говорится: на безрыбье и сам раком станешь :)
Топикстартеру нужно ООП "как в Java", при этом в CL ООП устроено иначе и методы отвязаны от классов (тут только поля) и создаются независимо. Вместо самопальной реализации Java-like ООП действительно стоит подумать в сторону решения задачи с использованием возможностей CL.
Типизация в CL есть, SBCL при (optimize (safety 3)) вполне себе изображает параноика. Также есть deftype, и использование его результатов в декларациях. Задачу "узлы - это функции, на которые я и хочу накладывать ограничения" вполне решает, так как сигнатуру проверит уж точно. Чтобы не писать лишнего можно действительно завернуть defun в подходящий макрос с declaim.
Писать Haskell на CL не предполагается, так что всех примирит пример use case вида "в FW есть это, потом приходит сторонний разработчик и пытается сделать это, а его бьют по рукам". Всё равно нужно держать в уме отличие лисп-системы и образа от однажды скомпилированного кода. Если всё равно будет "ненадёжно", то выбрать средство по душе.
@EO: я, признаться, последний абзац не понял. Можно разъяснить? На всякий случай повторю, что меня интересует способ наложения ограничений на функции, а не "ООП как в джаве" или статическая система как в хаскеле любой ценой. Мне нужно понять, каков lisp-way. Пример с деклеймом я понял, хочу разобраться, это лучший вариант или есть что-то еще, что мне еще не приходило в голову. Кстати, я привел ссылку в 10:34 на форум, где был задан именно мой вопрос. Народ предлагал в-принципе подобное. Еще аспекты вспомнили. И Flavors.
@m4: еще не смотрел
@m4: Посмотрел, спасибо. Интересное решение: вытянуть интерфейсы в параметры функций. Немного не то, что я себе представлял, но возможно, что именно это и есть решение , просто необычное и неожиданное. Буду думать.
Объяснение автора в конце интересное, пересекается с моей задачей:
2.4 Why And Wherefore
The proximate trigger for what became this article was a study we made on how to introduce modularity in the overly monolithic code base we were working on; we started the library as a proof of concept of our proposal for introducing parametric polymorphism in Common Lisp. Interestingly, though, the idea of detaching behavioral meta-data about objects in an entity separate from their state data and passed as an extra argument dates from our very first dabbling in implementing an Object Oriented language; indeed, our dissatisfaction with how traditional object-oriented style conflates behavior and state in the same “object” package-deal dates from the same time, as we were trying to figure out semantics for object systems and ways to modularly express mathematical concepts.
Про последний абзац - я сторонник начинать с простого и понятного, то есть описать простейший конкретный пример с указанием, что есть в FW, что должен делать пользователь FW, а чего не должен. Что подумалось по размытой фотографии: если у нас над неким классом сущностей предполагается совершать операции op1, op2,..., реализация которых ложится на плечи пользователя FW, то можно сделать макросы def-op1, def-op2 и т.д., внутри которых встроенными средствами CL будут проверяться контракты отдельных операций. Можно(?) сделать некий набор универсальных тестов, которые будут проверять полноту и корректность реализации этих макросов для типичных use cases.
IMHO Lisp-way - адаптировать язык под задачу. Шаг 1 - определиться с перспективой. Если она реально большая, то можно пойти по пути реализации "высоких" абстракций (модифицировать CLOS, чтобы ввести интерфейсы , например) и получить большой и серьёзный DSL. Если она небольшая, то сэкономить время пользователя FW и дать ему простые инструменты для проверки полноты и корректности реализации (моя догадка выше) - набор функций и минимум макросов (микро-DSL, условно). Поэтому и нужен конкретный пример. Шаг 2 - доработка в REPL (прикинуться пользователем). Сделать нечто, покрутить наживую, отрефакторить, стабилизировать интерфейс и закрыть ненужные возможности от случайного использования.
Разобраться, что лучше для конкретной задачи можно на примере того же простейшего (но "правильного") use case. Рисуем прототипы на основе понравившихся подходов и смотрим, что получается. Контракты все сразу проверять не обязательно - просто реализовать пару раз и держать в уме, что это надо будет сделать везде. Но обязательно довести прототип до реальной работы (пусть ограниченной и примитивной).
> IMHO Lisp-way - адаптировать язык под задачу> Шаг 1...
> Шаг 2...
Данному описанию очень не хватает ссылок на "success story", которые можно пощупать и посмотреть (например, которые можно установить у себя с помощью quicklisp).
Увы, детали показать не могу. Но если есть альтернативные открытые варианты, я только "за".
В свете твоих старых(?) взглядов на тему DSL скажу, что на термине и полной адаптации языка под задачу не настаиваю. Удобный ортогональный набор функций в библиотеке с моей т.з. тоже DSL, так как позволяет удобно и понятно решать задачи.
А шаг 1 - хотя бы примерное проектирование с прототипированием для выбора стека, а потом шаг 2 разработка и отладка в REPL какие вопросы вызывают?
>на термине и полной адаптации языка под задачу не настаиваю. Удобный ортогональный набор функций в библиотеке с моей т.з. тоже DSL, так как позволяет удобно и понятно решать задачи.
А разве это не является с точностью до наоборот: попыткой адаптировать задачу под язык?
Мне одному только кажется, что все эти призывы на каждый чих писать новый дсл, выглядят скорее как теория заговора против лиспа, чем попытка упростить разработку? Я считаю, что дсл должен сокращать время разработки (за счет сокращения рутинных операций или оптимизации последних), а не наоборот. Т.к. затраты на реализацию дсл (как и любые попытки адаптации языка под задачу) могут в разы превысить ценность самого проекта. Тут необходимо все хорошо взвесить. Поэтому, если есть готовое и хорошо документированное решение, то его и нужно использовать. А иначе, довольствоваться тем, что есть.
Кстати, сложные макросы я стараюсь вообще не писать, ибо затраты на их написание и документирование зачастую превышают пользу от их применения.
> А разве это не является с точностью до наоборот: попыткой адаптировать задачу под язык?
Давайте тогда определимся с терминами поточнее. Удобно решать - быстро, понятно, расширяемо, мало кода. В лиспе с хорошей библиотекой это будет выглядеть как короткое описание происходящего, в силу специфики синтаксиса похоже на eDSL.
>Мне одному только кажется, что все эти призывы на каждый чих писать
новый дсл, выглядят скорее как теория заговора против лиспа, чем попытка
упростить разработку? Я считаю, что дсл должен сокращать время
>разработки (за счет сокращения рутинных операций или оптимизации
последних), а не наоборот. Т.к. затраты на реализацию дсл (как и любые
попытки адаптации языка под задачу) могут в разы превысить ценность
>самого проекта. Тут необходимо все хорошо взвесить. Поэтому, если есть
готовое и хорошо документированное решение, то его и нужно использовать.
А иначе, довольствоваться тем, что есть.
Совершенно согласен. Но свои затраты нормально можно оценить по опыту или прототипированием. Почему под DSL по умолчанию понимается что-то эпическое?
> В свете твоих старых(?) взглядов на тему DSL
Мои взгляды очень просты, мне не нравится подход "в любой непонятной ситуации делай DSL".
Тогда сокращу своё мнение до "в любой непонятной ситуации сначала сделай пару прототипов".
В конце концов я сам нашёл ответ на вопрос, в лучшем стиле stackOverflow, где поощряется самостоятельный поиск решения и публикация ответа на свой же вопрос.Короче, тут всё описано:
5-2-03 Strong Typing vs. Strong Testing // link: https://docs.google.com/document/d/1aXs1tpwzPjW9MdsG5dI7clNFyYayFBkcXwRDo-qvbIk/preview
далее идёт стек ссылок как я попал на этот сайт:
Hacker News // link: https://news.ycombinator.com/submitted?id=malisper
Macrology // link: http://malisper.me/about-this-site-2/
Planet Lisp: // link: http://planet.lisp.org
Ещё один вариант.
Смотреть слайд 58 и далее по контексту.