Alterator/objects
Объектная система alterator
В текущей нестабильной ветке alterator произошло большое изменение: там внедрены объекты. Зачем это нужно? Нужно это для того, чтобы поддерживать код alterator было легче, чтобы убрать старые хаки, уменьшить количество cond и case, затрудняющих чтение. Что это такое? Читайте далее...
Объекты — это замыкания для бедных
Эта цитата принадлежит Norman Adams и сейчас вы убедитесь насколько это верно ;)
Любая созданная процедура запоминает своё окружение вместе со всеми определёнными там переменными и другими функциями. При этом запоминает настолько хорошо, что куда бы далее вы ни передавали эту процедуру, она будет работать именно с тем окружением, в котором родилась (подобно тому как цыплята, кого увидят первым, вылупившись из яйца, того и считают мамой), данные окружения будут существовать до тех пор, пока они требуются хотя бы одной процедуре. Например:
(define (make+ x)
(lambda (y) (+ x y)))
(define x 10)
(define +three (make+ 3))
(+three 5) ==> вернёт восемь.
(define +five (make+ 5))
(+five 5) ==> вернёт десять.
Здесь функция +three, появившись, запомнила, что x — это 3, и при всех последующих запусках будет работать именно с этим значением, даже если мы явно зададим x значение 10.
Итак, у нас есть «память», а если есть «память», значит можно сделать функции с состоянием, иначе говоря объекты. Вот например простейший объект, изображающий точку на плоскости:
(define (make-point-2d x y)
(lambda (op . value)
(case op
((get-x) x)
((get-y) y)
((set-x) (set! x (car value)))
((set-y) (set! y (car value))))))
(define point (make-point-2d 3 4))
(point 'get-x) ==> вернёт 3
(point 'set-x 10) ; запомнит в своём окружении новое значение x
(point 'get-x) ==> вернёт 10
Вот теперь ясно видно, что имея замечательное свойство «памяти», можно создавать то, что в других языках программирования называют объектами, то есть совокупность данных и методов, работающих с этими данными. То, как именно строить объекты — никто вас не ограничивает, поэтому существует множество вариантов объектных систем для Scheme. Система объектов Alterator похожа на объектную систему T.
Методы или сообщения
Работают с объектами, как правило, одним из двумя способами: явно обращаясь в методам или данным или передавая объекту сообщение. Эти способы совершенно равноценны. Просто в одном языке удобнее вызвать метод:
object.method(params)
В другом, послать объекту сообщение с желанием выполнить метод:
(send-message object 'method params)
Как вы догадываетесь, в LISP принято использовать второй способ.
Простые объекты
На вводимые дальше объекты можно смотреть как на существенно улучшенные функции. С одной стороны объект будет содержать в себе собственно тело функции, с другой стороны у него будет "вторая дверь", через которую можно будет вызывать определённые в объекте операции. То есть мы получаем объект "функция с рычагами".
Общий формат процедуры создания объектов следующий:
(object <proc> <methods>)
<proc> - выражение которое будет исполняться при каждом вызове объекта (дверь #1). <methods> - набор определённых операций (дверь #2). Операций может и не быть. Вырожденный случай (есть только дверь #1):
(object (lambda args #f))
Это тоже самое что и просто процедура:
(lambda args #f)
Методы описываются в следующем формате:
((name self args) body)
Здесь:
- name - имя операции.
- self - ссылка на сам объект. Очень полезно когда из одного метода хочется вызвать другой метод того же объекта.
- args - необязательные дополнительные параметры метода.
- body - тело метода.
Операции
Как же войти во вторую дверь? Для этого существуют так называемые операции. Создаётся операция при помощи инструкции (operation имя) Можно сразу создать и определить операцию (define-operation имя). Последняя конструкция полностью аналогична операции (define имя (operation имя))
Создадим объект точки на плоскости:
(define (make-point-2d x y) (object 'called ((get-x self) x) ((get-y self) y) ((set-x self value) (set! x value)) ((set-y self value) (set! y value)))) (define-operation get-x) (define-operation set-x) (define point (make-point-2d 3 4)) (point) ==> при исполнении вернёт символ 'called как и просили (get-x point) ==> вернёт 3 (set-x point 10) ==> запомнится новое значение x (get-x point) ==> вернёт 10
У процедуры operation есть ещё один необязательный параметр - действие по умолчанию. В случае если та или иная операция не была обнаружена, то будет запущено действие по-умолчанию. Если действие по умолчанию не определено, а требуемая операция в объекте не обнаружена, то будет выдано сообщение об ошибке и исполнение кода прервётся. Небольшой пример:
(define-operation op 333) ; операция с действием по-умолчанию - вернуть число 333 (define-operation op2); операция без действия по-умолчанию (op (object #f ((op self) 444))) ==> вернёт 444, ибо операция обнаружена и исполнена (op (object #f ((op2 self) 555))) ==> вернёт 333, ибо операция не обнаружена, но есть действие по-умолчанию. (op2 5) ==> ошибка! операции не найдено (op 5) ==> вернёт 333.
Это неприметное на первый взгляд действие по-умолчанию позволяет творить чудеса. Вот так например можно сделать предикат различающий только интересующие нас объекты
(define-operation obj? #f) (define obj (object #f ((obj? self) #t)))) (obj? obj) ==> вернёт #t (obj? '(1 2 3)) ==> вернёт #f
Возможны и другие фокусы ....
Составные объекты
Можно усложнять "маршрутизацию сообщений", тем самым создавая то, что в других языках программирования называют "наследованием" и "иерархией объектов".
Для комбинирования объектов используется процедура join. В результате применения join объектам (object proc1 method11 method12 ...) (object proc2 method21 method22 ...) Получится объект (object proc1 method11 method12 ... method21 method22 ... ).
Создадим объект точки в пространстве:
(define (make-point-3d x y z) (join (make-point-2d x y) (object #f ((get-z self) z) ((set-z self value) (set! z value))))) (define-operation get-x) (define-operation get-z) (define point (make-point-3d 3 4 5)) (get-x point) ==> вернёт 3 (get-z point) ==> вернёт 5 (point) ==> вернёт 'called как если бы это был вызван point-2d. (join 1 (make-point-3d 2 3 4)) ==> а можно делать и такие вот "соединения", при вызове этот объект будет возвращать 1, а по операциям будет совпадать с объектом типа point3d.
Все примеры выше не являются по сути вызываемыми объекты. Где же могут быть полезны настоящие двудверные функции-объекты?
Возьмём пример из самого alterator.
Создадим объект "пустой атрибут". Если у него спросить имя атрибута, то он его вернёт, а любые попытки вызова интерпретируются как пожелание задать данному атрибуту значение и в результате возвращается объект "атрибут со значением". Последний также воспринимает сообщения на предмет получения его имени и значения, а все попытки вызова интерпретирует как пожелание добавить ещё одно значение, то есть в результате возвращается атрибут с расширенным значением. Пример:
width --> это пустой атрибут (name-of width) ==> возвращает имя атрибута, то есть width (width 10) ==> возвращает "атрибут со значением" 10 (name-of (width 10)) ==> имя width (value-of (width 10)) ==> значение 10 ((width 10) 20) ==> "атрибут со значением" (10 20).
Такое поведение очень удобно использовать в описании интерфейса
(label "aaa" width 20) (some-widget (dimensions 10 50) color 'blue ...)
Описываются эти необычные объекты очень и очень просто:
(define (make-empty-attribute attr-name) (set-instance! '<empty-attribute> (object (lambda args (apply make-attribute attr-name args)) ((name-of self) attr-name)))) ;create attribute with value (define (make-attribute attr-name . attr-value) (set-instance! '<attribute> (object (lambda args (apply make-attribute attr-name (append attr-value args))) ((name-of self) attr-name) ((value-of self) attr-value))))
Ну вот и всё. Объектная система простая - как и всё в Scheme.
Объекты в alterator
Какие же объекты применяются в самом alterator?
- <attribute> — применяется в описании интерфейса, олицетворяет собой атрибут с заданным значением, позволяет делать записи вида: (vbox (with 10) (height 20)). Это полезно когда надо задать в атрибуте сразу несколько значений.
- <container> — контейнер с вложенными контейнерами и свойствами. Превращается в тот или иной виджет в интерфейсе.
Также применяются объекты для нативных бэкендов alterator.
….