Alterator/objects

Материал из ALT Linux Wiki
< Alterator
Версия от 19:00, 28 июля 2008; EvgenySinelnikov (обсуждение | вклад) (Import from freesource.info)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Freesource-logo.png Blue Glass Arrow.svg MediaWiki logo.png
Эта страница была перемещена с freesource.info.
Эта страница наверняка требует чистки и улучшения — смело правьте разметку и ссылки.
Просьба по окончанию убрать этот шаблон со страницы.


Объектная система 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.

....