Изменение метода отправки в Common Lisp

Я пытаюсь смоделировать что-то похожее на классы типов Хаскелла с CLOS Common Lisp. То есть я хотел бы иметь возможность отправлять метод для "классов типов" объекта вместо его суперклассов.

У меня есть метакласс, определенный для классов, которые имеют и реализуют классы типов (которые являются просто другими классами). Те классы (которые реализуют классы типов) имеют слот, содержащий список реализуемых ими классов типов.
Я хотел бы иметь возможность определять методы для класса типов, а затем иметь возможность отправлять этот метод на объекты, класс которых реализует этот класс типов. И я хотел бы иметь возможность добавлять и удалять классы типов динамически.

Я полагаю, что мог бы сделать это, изменив алгоритм отправки метода, хотя это не кажется слишком простым.

Кому-нибудь достаточно удобно с CLOS и MOP, чтобы дать мне несколько советов?

Благодарю.

Изменить: мой вопрос может быть указан как, как я могу реализовать compute-applicable-methods-using-classes а также compute-applicable-methods для "пользовательского" класса универсальной функции, такого, что если некоторые из специализаторов метода универсальной функции являются классами типов (классы, метакласс которых является классом "класса типов"), то класс соответствующего аргумента должен реализовывать класс типов (который просто означает наличие класс типов хранится в слоте класса аргумента), чтобы метод был применим?
Из того, что я понимаю из документации, когда вызывается универсальная функция, compute-discriminating-functionсначала вызывается, который сначала попытается получить применимые методы через compute-applicable-methods-using-classesи в случае неудачи попробует то же самое с compute-applicable-methods,
Хотя мое определение compute-applicable-methods-using-classes кажется, работает, универсальная функция не может отправить применимую функцию. Так что проблема должна быть в compute-discriminating-function или же compute-effective-method,

Смотрите код.

2 ответа

Решение

Это не легко достижимо в Common Lisp.

В Common Lisp операции (универсальные функции) отделены от типов (классов), т.е. они не "принадлежат" типам. Их отправка выполняется во время выполнения, с возможностью добавления, изменения и удаления методов также во время выполнения.

Обычно ошибки от отсутствующих методов сигнализируются только во время выполнения. Компилятор не может знать, хорошо ли используется универсальная функция или нет.

В Common Lisp идиоматическим способом является использование универсальных функций и описание их требований, или, другими словами, наиболее близким к интерфейсу в Common Lisp является набор универсальных функций и класс маркера mixin. Но чаще всего указывается только протокол и его зависимости от других протоколов. Смотрите, например, спецификацию CLIM.

Что касается классов типов, это ключевая особенность, которая сохраняет язык не только полностью безопасным для типов, но и делает его очень расширяемым в этом аспекте. В противном случае, либо система типов будет слишком строгой, либо отсутствие выразительности приведет к небезопасным ситуациям, по крайней мере, с точки зрения компилятора. Обратите внимание, что Haskell не сохраняет или не должен сохранять типы объектов во время выполнения, он принимает каждый вывод типов во время компиляции, что сильно отличается от идиоматического Common Lisp.

Чтобы иметь что-то похожее на классы типов в Common Lisp во время выполнения, у вас есть несколько вариантов

Если вы решите поддерживать классы типов с его правилами, я предлагаю вам использовать протокол мета-объекта:

  • Определите новый мета-класс универсальной функции (то есть тот, который наследуется от standard-generic-function)

  • специализироваться compute-applicable-methods-using-classes возвращать false как второе значение, потому что классы в Common Lisp представлены исключительно своим именем, они не являются "параметризуемыми" или "ограничиваемыми"

  • специализироваться compute-applicable-methods проверять мета-классы аргумента на наличие типов или правил, отправлять их соответствующим образом и, возможно, запоминать результаты

Если вы решите использовать только параметризуемые типы (например, шаблоны, шаблоны), существующей опцией будет библиотека интерфейса Lisp, где вы передаете объект, который реализует определенную стратегию с использованием протокола. Тем не менее, я рассматриваю это главным образом как реализацию шаблона стратегии или явную инверсию управления, а не как фактические параметризуемые типы.

Для реальных параметризуемых типов вы можете определить абстрактные непараметризованные классы, из которых вы будете интернировать конкретные экземпляры со смешными именами, например lib1:collection<lib2:object>, где collection абстрактный класс, определенный в lib1 пакет, и lib2:object на самом деле часть имени, как и для конкретного класса.

Преимущество этого последнего подхода заключается в том, что вы можете использовать эти классы и имена в любом месте CLOS.

Основным недостатком является то, что вы все равно должны генерировать конкретные классы, так что у вас, вероятно, будут свои defmethod -подобный макрос, который будет расширяться в код, который использует find-class -подобная функция, которая знает, как это сделать. Таким образом, нарушая значительную часть преимущества, которое я только что упомянул, или иначе вы должны следовать дисциплине определения каждого конкретного класса в вашей собственной библиотеке, прежде чем использовать их в качестве специалиста.

Другим недостатком является то, что без дальнейшей нетривиальной сантехники это слишком статично, не является действительно общим, так как не учитывает, что, например, lib1:collection<lib2:subobject> может быть подклассом lib1:collection<lib2:object> или наоборот. В целом, он не принимает во внимание то, что известно в информатике как ковариация и контравариантность.

Но вы могли бы реализовать это: lib:collection<in out> может представлять абстрактный класс с одним контравариантным аргументом и одним ковариантным аргументом. Трудной частью было бы создание и поддержание отношений между конкретными классами, если это вообще возможно.

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


РЕДАКТИРОВАТЬ: После редактирования вашего вопроса, я должен сказать, что compute-applicable-methods-using-classes должен вернуться nil в качестве второго значения всякий раз, когда в методе есть специализатор класса типа. Вы можете call-next-method иначе.

Это отличается от того, что в применимом методе есть специализатор класса типов. Помните, что CLOS ничего не знает о классах типов, поэтому, возвращая что-то из c-a-m-u-c с истинным вторым значением, вы говорите, что все в порядке, чтобы запоминать (кэшировать) только данный класс.

Вы должны действительно специализироваться compute-applicable-methods для правильной диспетчеризации классов типов. Если есть возможность для запоминания (кэширования), вы должны сделать это сами здесь.

Я считаю, что вам нужно переопределить compute-applicable-methods и / или compute-applicable-methods-using-classes которые вычисляют список методов, которые понадобятся для реализации вызова общей функции. Вам тогда, вероятно, придется переопределить compute-effective-method который объединяет этот список и несколько других вещей в функцию, которая может быть вызвана во время выполнения для выполнения вызова метода.

Я действительно рекомендую прочитать "Протокол протокола метаобъекта" (как уже упоминалось), в котором подробно рассказывается об этом. Подводя итог, предположим, что у вас есть метод foo определены в некоторых классах (классы не должны быть связаны каким-либо образом). Оценка кода LISP (foo obj) вызывает функцию, возвращаемую compute-effective-method который проверяет аргументы, чтобы определить, какие методы вызывать, а затем вызывает их. Цель compute-effective-method заключается в устранении как можно большей части затрат времени выполнения путем компиляции тестов типов в оператор case или другое условное выражение. Таким образом, среда выполнения Lisp не должна запрашивать список всех методов при каждом вызове метода, а только при добавлении, удалении или изменении реализации метода. Обычно все это делается один раз во время загрузки, а затем сохраняется в образе lisp для еще большей производительности, в то же время позволяя вам изменять эти вещи, не останавливая систему.

Другие вопросы по тегам