Языки LOOPS и FLAVORS
Объектно-ориентированный стиль программирования идеально подходит для решения проблем, требующих детального представления объектов реального мира и динамических отношений между ними. Классическим примером применения данного подхода являются задачи моделирования. В таких программах компоненты сложной системы представляются структурами, инкапсулирующими и данные, и функции, моделирующие поведение соответствующих компонентов. Первым языком, в котором была реализована такая идея, стал SmallTalk [Goldberg andRobson, 1983].
Для задач искусственного интеллекта были разработаны языки LOOPS и FLAVORS, причем оба представляли собой объектно-ориентированные расширения языка LISP. Хотя в настоящее время эти языки практически не используются, реализованные в них базовые идеи унаследованы множеством языков представления знаний, появившихся позже. В частности, это можно сказать о языках CLOS (Common LISP Object System) и CLIPS. Ниже мы кратко опишем основные функциональные возможности языков LOOPS и FLAVORS и обратим ваше внимание на некоторые сложности, связанные с реализацией объектно-ориентированного стиля программирования.
Передача сообщений
Идея объектно-ориентированного программирования состоит в том, что программа строится вокруг множества объектов, каждый из которых обладает собственным набором функций (операций). Вместо того чтобы представлять объект пассивным набором данных, объектно-ориентированная система позволяет объекту играть более активную роль, в частности взаимодействовать с другими объектами, обмениваясь с ними сообщениями. В результате основной упор переносится с разработки общей управляющей структуры программы, которая ответственна за порядок обращения к функциям, на конструирование самих объектов, выяснение их ролей и создание протоколов взаимодействия объектов. Эти протоколы, по существу, определяют интерфейс между объектами. Если один объект должен взаимодействовать с другим, он должен вызывать функции в строгом соответствии с этим интерфейсом.
Объекты располагают собственными данными, которые играют ту же роль, что и слоты фреймов, собственным механизмом обновления этих данных и использования хранящейся в них информации. Помимо функций интерфейса, объекты располагают собственными, "приватными" функциями, которые, как правило, представляют собой реализацию определенной родовой операции применительно к данному объекту. Помимо данных, передаваемых в качестве аргументов родовой операции, такие функции используют и локальные данные объекта – аналоги слотов фрейма.
Предположим, мы определили объект, представляющий класс ship (корабль), и наделили его свойствами x-velocity (скорость по х) и y-velocity (скорость по у). Теперь можно создать экземпляр класса ship, назвать его Titanic и одновременно присвоить свойствам x-velocity и y-velocity нового экземпляра исходные значения. Практически нет никаких отличий между этой процедурой и процедурой создания нового экземпляра фрейма, рассмотренной в предыдущей главе.
Предположим теперь, что нам понадобилось определить процедуру speed, которая будет вычислять скорость судна на основании значений свойств x-velocity и у-velocity (скорость вычисляется как корень квадратный из суммы квадратов компонентов). Такая процедура будет принадлежать абстрактному типу данных, представляющему любые суда (в терминологии языка SmallTalk speed – это метод класса ships, а в терминологии C++ – функция-член класса ships.)
Идея состоит в том, чтобы закодировать в объекте (классе) не только декларативные знания о судах, но и процедурные, т.е. методы использования декларативных знаний. Для того чтобы активизировать процедуру вычисления скорости определенного судна, в частности "Титаника", нужно передать объекту Titanic сообщение, которое побудит его обратиться к ассоциированной процедуре в контексте данных о компонентах скорости именно этого объекта. Titanic – это экземпляр класса ships, от которого он унаследовал процедуру speed. Все это представляется довольно очевидным, но описанный механизм срабатывает только в случае, если соблюдаются следующие соглашения.
Во-первых, программа, разработанная в расчете на этот механизм, должна "учредить" и следовать в дальнейшем определенному протоколу или "контракту", определяющему способ взаимодействия между объектами. Другими словами, интерфейс обмена сообщениями между объектами должен быть досконально продуман, а правила этого интерфейса жестко соблюдаться. Лучше всего продемонстрировать эту мысль на примере.
Для того чтобы определить компоненту X текущего положения "Титаника", программа должна послать запрос объекту Titanic, который имел бы следующий смысл: "передай текущее значение координаты X. Как в объекте формируется это значение или как оно хранится – дело только самого объекта и никого более не касается. Ни объекты других классов, ни какие-либо другие компоненты программы этого не знают. Более того, внутренний механизм доступа к этой информации должен быть скрыт, чтобы никто не мог добраться к ней, минуя сам объект. Это соглашение принято называть инкапсуляцией.
Во-вторых, совершенно очевидна избыточность определения своего метода вычисления скорости для каждого класса объектов, которые обладают возможностью перемещаться в двумерной системе координат. Метод, который мы только что определили для класса судов, с таким же успехом может быть использован и для других движущихся объектов, поскольку вычисление скорости представляет собой родовую операцию. Поэтому имеет смысл связать этот метод с каким-нибудь суперклассом транспортных средств, производными от которого будут классы судов, автомобилей, троллейбусов и т.п. Все эти подклассы унаследуют родовую операцию у своего базового класса.
Такое наследование – это уже нечто большее, чем когнитивная экономия или наследование свойств. Выполнение родовых операций встраивается в механизм обмена сообщениями. Отсылка сообщения – это отнюдь не вызов определенной процедуры, поскольку вызывающий объект не знает, каким именно методом отреагирует на это сообщение объект-получатель, от кого он унаследует этот метод, и будет ли вообще задействован механизм наследования в данном конкретном случае. Вызывающему объекту известны лишь наименование операции и ее внешние по отношению к преемнику аргументы. Все остальное – заботы объекта-реципиента сообщения.
Формирование объекта класса на языке CLIPS
Ниже показано, как на языке CLIPS определяется класс ship и формируется экземпляр этого класса titanic. Сначала определим класс ship, в котором имеются два слота: x-velocity и y-velocity:
(defclass ship (is-a INITIAL_OBJECT) (slot x-velocity (create-accessor read-write)) (slot y-velocity (create-accessor read-write)))
Теперь сформируем экземпляр этого класса,, которому будет дано наименование "Titanic". Проще всего это сделать с помощью функции definstaces, которая в качестве аргументов принимает список параметров формируемых экземпляров. Определенные таким способом экземпляры класса будут инициализироваться при каждом перезапуске интерпретатора CLIPS.
(definstances ships (titanic of ship (x-velocity 12) (y-velocity (10)
Завершается определение созданием обработчика событий для класса ship. Все экземпляры класса будут использовать этот обработчик для вычисления собственной скорости. Обратите внимание на то, что член в этом определении ссылается на значение слота того экземпляра класса, скорость которого требуется вычислить.
(defmessage-handler ship speed () (sqrt (+ {?self:x-velocity?self:x-velocity) (?self:y-velocity?self:y-velocity))) )
Если файл со всеми представленными выше выражениями загрузить в среду CLIPS, а затем ввести с клавиатуры (send [titanic] speed), то в ответ интерпретатор CLIPS выведет скорость объекта titanic.