ООП на Лиспе
При переходе от обычного стандартного программирования с ООП связывают радикальное изменение способа организации программ. Это изменение произошло под давлением роста мощности оборудования. ООП взламывает традиционное программирование по многим направлениям. Вместо создания отдельной программы, оперирующей массой данных, приходится разбираться с данными, которые сами обладают поведением, а программа сводится к простому взаимодействию новой категории данных — «объекты».
Чтобы сравнить дистанцию с функциональным программированием, рассмотрим самодельный встроенный в Лисп объектно-ориентированный язык (ОО-язык), обеспечивающий основы ООП. Встраивание ОО-языка — идеальный пример, показывающий характерное применение функционального программирования, при котором типичные понятия ООП отображаются в фундаментальные абстракции.
При организации наследования следует пояснить разницу между моделями обобщенных функций и обмена сообщениями.
-
объекты обладают свойствами,
- посылают сообщения,
- наследуют свойства и методы от предков.
Рассмотрим программу вычисления площадей ряда геометрических фигур, таких как круги, прямоугольники, звезды и пр. Ее запись в ОО-стиле поначалу может выглядеть непринципиально отличающейся от обычной программы.
Но допустим, понадобилось ввести новый класс — раскрашенные круги. Если методы раскраски произвольных фигур уже существовали, то можно унаследовать две особенности:
- от круга наследуем понятие «радиус», а от раскрашенных — «цвет»;
- можно не вводить метод «раскраска круга».
Практически ООП — это организация программ в терминах методов, классов, экземпляров и наследования. Почему стоит писать программы таким способом? Основной выигрыш — программы легче изменять. Если мы хотим изменить способ манипулирования каким-либо объектом некоторого класса, то мы изменяем лишь метод этого класса. Если мы хотим сделать нечто подобное объекту, но отличающееся в отдельных чертах, мы можем создать подкласс объектов, а уже для подкласса поменять его отдельные черты.
Если программа написана тщательно, то можно добиться того, чтобы все такие модификации производились даже без просмотра ранее написанного исходного текста программы.
В Лисп есть разные способы размещать коллекции свойств. Один из них — представлять объекты как хэш-таблицы и размещать свойства как входы в нее. В [7] приведен блестящий пример реализации ООП на базе хэш-таблиц. Тогда отдельное свойство можно получать из таблицы в форме:
(gethash 'color obj)
Поскольку функции являются данными объекта, мы можем размещать их точно так же, как и свойства. Это значит, что мы должны завести еще методы, позволяющие вызывать данный метод объекта как исполнение свойства данного объекта.
(funcall (gethash 'move obj) obj 10)
Мы можем определить под эту идею синтаксис как в Smalltalk
(defun tell (obj message &rest args) (apply (gethash messmage obj) obj args))
что позволяет сказать объекту, чтобы он переместился на 10 шагов, в форме:
(tell obj 'move 10)
Фактически успех наследования обеспечивает единственная особенность Лиспа: все это работает благодаря реализации рекурсивной версии GETHASH. Таким образом, определение данной функции нам сразу даст все три основные черты ООП.
Посмотрим, во что это обойдется в исходном примере.
Мы должны создать два объекта, один — потомок другого.
В объект «круги» мы помещаем методы для всех кругов. Для начала это функция одного аргумента — объекта, посылающего сообщение:
(setf (gethash 'area circle-class) #' (lambda (x) (* pi (expt (rget ' radius x) 2))))
Теперь можно спрашивать о площади круга, она будет вычисляться согласно методу, определенному для класса. Мы используем rget при чтении свойства и tell — при вызове метода.
(rget 'radius our-cicle) (tell our-circle 'area)
Прежде чем улучшать эту программу, надо проверить, что же получилось. Легкость результата — это трюк, но не программистский трюк, а концептуальный. Не будем забывать о том, что Лисп по своей природе уже был ОО-языком, или даже чем-то более общим изначально. Все что нам понадобилось — это создать новый фасад для уже существующих в Лиспе абстракций.