Опубликован: 04.04.2012 | Уровень: для всех | Доступ: платный
Лекция 6:

Агенты как функциональный тип данных

< Лекция 5 || Лекция 6: 1234 || Лекция 7 >
Аннотация: В лекции рассматривается новый тип данных, называемый функциональным типом. Объектами этого типа являются функции и процедуры – методы класса. В языке Eiffel вводится понятие агента, при определении которого с агентом связывается некоторый метод класса. Агентное выражение может быть присвоено переменной функционального типа. Механизм агентов находит широкое применение в различных проблемных областях. В лекции подробно обсуждаются вопросы, связанные с заданием агентов. Обсуждаются важные примеры применения агентов в задачах итерирования, численного интегрирования, построения командных систем с встроенным механизмом откатов – redo и undo.

За пределами двойственности

Расширение потребует рассмотрения операций так, как если бы они были объектами. С первого взгляда это противоречит базисной двойственности этих двух понятий.

  • Программы манипулируют объектами.
  • Они делают это, применяя операции к объектам.

Текстуальная структура наших ОО-программ также основана на этом различии: мы разделяем программы на классы, в основе каждого из которых лежит некоторый тип объектов. Каждая операция присоединяется к классу в виде программы — метода класса.

Кажется, что эти два понятия четко различаются: то, что программа может делать, — это операция, а действия выполняются над объектами.

И все же иногда интересно рассматривать операцию как объект или, более точно, определять объекты, чья единственная роль состоит в описании операции. Мы называем такие объекты агентами. В этой лекции они детально рассматриваются, но базисную идею изложить нетрудно прямо сейчас. Простого агента можно получить, используя нотацию

agent r

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

a := agent r

Здесь переменная a должна иметь подходящий тип.

Что мы можем делать с агентами? Понятно, что агент ассоциирован с компонентом (методом), поэтому одно из его использований состоит в вызове этого компонента. После вышеприведенного присваивания a обозначает агента, поэтому возможен вызов

a.call ([x, у])
Листинг 5.1.

Эффект от вызова такой же, как и от непосредственного вызова программы r для любых применимых x и у

r (x, у) 
Листинг 5.2.

Метод call применим ко всем агентам, он принимает в качестве аргумента единственный кортеж, здесь [x, у]. Кортеж — это просто объект, представляющий последовательность значений (надеюсь, вы помните это, но если нет, то прежде чем продолжить чтение, перечитайте раздел 13.5).

Зачем использовать вызов метода call с агентом, как в [5.1], вместо прямого вызова [5.2]? Действительно, если известно, какую процедуру вы хотите вызвать, то пользоваться услугами агента нецелесообразно. Но теперь предположите, что a получено от другого программного элемента, например, как аргумент текущего метода. Тогда вы знаете только, что a обозначает некий метод (и, как мы увидим далее, какие типы аргументов у этого метода), но не знаем самого метода, в данном примере r.

Агенты дают нам возможность построить и поставлять объекты, представляющие операции, готовые к выполнению, с полным разделением между:

  • определением агента — точкой ПО, где агент связывается с известной программой r, выполняя agentr ;
  • вызовом агента call — любой точкой ПО, которая получает агента a и может применить call для вызова агента, не зная точно, какую программу он принесет.

Механизм имеет много различных применений. В данный момент проанализируем несколько наиболее важных.

  • Итерация: обеспечение общего механизма, который применяет произвольную операцию к каждому элементу структуры данных.
  • Численное программирование: подынтегральная функция может рассматриваться как агент при написании метода вычисления определенного интеграла на заданном интервале.
  • Поставка интерактивного приложения с механизмом отката: undo-redo.

Еще одна область, где агенты играют важную роль, — это программирование, управляемое событиями: образец, известный под именем "Писатель-Читатели" или "Издатель-Подписчики", полезный, в частности, для реализации GUI (графического интерфейса пользователя), являющегося предметом следующей лекции.

Что нам предстоит изучить? Сравним агенты с другими приемами, основанными на ранее изученных механизмах, в частности, с динамическим связыванием, также применимым к некоторым из рассматриваемых применений агентов. Дадим краткий обзор математических основ — восхитительной теории лямбда-исчисления. Проэкзаменуем некоторые приемы, которые доступны в языках, отличных от Eiffel.

Зачем операции превращать в объекты?

Для начала хорошо бы понять, зачем нам нужно рассматривать операции как объекты и что было бы в отсутствии такой возможности. Рассмотрим четыре примера: итерацию, интегрирование, наблюдение, откат.

Четыре приложения агентов

Начнем с итерации. Нам приходилось использовать в циклах схему, где операция применяется к каждому элементу последовательной структуры, такой как список. Схема выглядит примерно так:

from start until after loop
  " Применить действие к элементу "
  forth
end
Листинг 5.3.

Здесь start устанавливает курсор на первом элементе, forth перемещает его к следующему элементу структуры, after позволяет выяснить, достигнут ли конец структуры, а item позволяет получить элемент, заданный курсором.

 Операции над списком

Рис. 5.1. Операции над списком

В Traffic эту схему можно применять к экземпляру ROUTE, обозначающему маршрут с остановками. Возможно, нам захочется распечатать список всех остановок в порядке следования их на данном маршруте. Возможно, нам необходимо подсчитать общее время прохождения маршрута (зная время, затрачиваемое на проезд от одной остановки до следующей). Возможна и такая экзотическая операция, как получение списка всех близлежащих ресторанов, зная, какие рестораны находятся в окрестности каждой станции. Для этих и многих других случаев общее решение соответствует схеме [5.3]. У нас есть имя для таких схем — итерация (или итерирование), уже встречавшееся при обсуждении структур данных. Можно использовать такую схему для любого действия, имея программу, выполняющую действие для каждой остановки вдоль маршрута.

Теперь предположим, что по лености, свойственной программистам, или помня, что дублирование кода — это признак плохого программирования, вы не хотите каждый раз писать новый метод, реализующий схему для нового действия. Можно ли подняться вверх по шкале абстракции и написать метод, подобный [5.3], где действие будет выступать в роли аргумента? Тогда мы могли бы применять этот метод для различных действий, возлагая на него заботу об итерировании.

Механизм итерирования на самом деле обеспечивает нам такой метод, названный do_all, которому при вызове передается агент, задающий действие:

your_route.do_all (agent action)

Наш второй пример из области вычислительной математики: интегрирование. Дана функция f(x: REAL): REAL. Функция определена на интервале [a, b]. Существует численный алгоритм (ниже будет дано его обоснование), позволяющий получить хорошую аппроксимацию интеграла от функции f на заданном интервале:

\int\limits_a^b f(x) dx

Проблема в том, чтобы определить в классе INTEGRATOR общий механизм интегрирования метод, названный, например, integral, допускающий повторное использование, чтобы его можно было бы применять для программ, задающих реализации различных математических функций. Агенты позволяют решить эту проблему, обеспечивая нужный механизм:

your_integrator.integral(agent f, a, b)

Здесь your_integrator имеет тип INTEGRATOR.

Третий пример предваряет рассмотрение следующей лекции: программирование, управляемое событиями. Предположим, что в некоторых частях программной системы могут возникать в процессе выполнения "события", а другие части системы в ответ на события должны выполнять определенные действия. Примером события может служить событие tick системных часов, уведомляющее об истечении очередного кванта времени. В ответ на это один модуль должен изменить визуальный образ часов, отображаемых на экране дисплея, другой модуль должен обновить свой счетчик времени и так далее. Каждый такой модуль — "подписчик" события — должен зарегистрировать некоторое действие, выполняемое всякий раз, когда происходит событие. Спроектировать архитектуру, позволяющую подписчикам достигать требуемого эффекта, достаточно просто при использовании агентов:

clock_tick.subscribe (agent some_routine)

Здесь clocktick представляет тип события, а subscribe — общецелевой библиотечный метод. О таких подписчиках говорят, что они "наблюдают" за событиями определенного типа.

Последний рассматриваемый нами пример соответствует функциональности, необходимой, как правило, любым интерактивным системам — операции "отката". Никогда не доводилось видеть памятник, установленный изобретателю функциональности, которая возникает при нажатии простой пары клавиш CTRL-Z, а ведь возможность отката — это один из краеугольных камней в истории человечества, учитывающий нашу необходимость обезопасить себя от собственных ошибок. Даже если и не иметь в виду путаницу, являющуюся следствием наших действий, часто просто хочется проверить некоторую идею, а затем вернуться назад, если полученный результат не устраивает.

По-настоящему хороший механизм undo-redo — это тот, который позволяет отменить не только последнюю операцию, но многие. Пользователю ПО не нужно долго пояснять, что делает механизм, но вот как он это делает, как самому реализовать программу со встроенным механизмом undo-redo — задача более сложная.

Наиболее радикальный способ включает представление всех действий, реализующих откаты и повторы, как объектов, которые можно поместить в структуру данных, скажем, history, которая может быть реализована как список из пар агентов:

Список истории

Рис. 5.2. Список истории

Каждое действие выполняется некоторым методом, назовем его r. В этом решении система никогда не вызывает непосредственно сам метод r для выполнения очередного действия. Вместо этого вызывается

execute (agent r, agent r_inverse )

Здесь execute выполняет вызов call (механизм вызова метода, ассоциированного с агентом) , используя первый аргумент, но также пару объектов, переданных как аргументы, записывает в список истории. Каждая пара в списке истории содержит два агента, один представляет действие, другой противодействие, — метод, отменяющий результат действия. Предполагается, что всякий раз, когда вы создаете метод r, реализующий некоторую команду, одновременно создается и метод r_inverse, отменяющий действие (в противном случае реализовать механизм откатов и повторов не удалось бы). Так что если пользователь запрашивает откат на несколько шагов, необходимо выполнить

history.item.reaction.call ([])
history.back

Эта пара операторов вызывается несколько раз, по числу шагов отката, но, конечно, не далее начала списка истории. Для повтора, запрашиваемого после одного или более отката, выполняется

history.forth
history.item.action.call ([])

Опять-таки повторы выполняются не далее последнего элемента.

< Лекция 5 || Лекция 6: 1234 || Лекция 7 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения". 

Юрий Симонов
Юрий Симонов
Россия, Москва, Московский Государственный Университет им. М.В. Ломоносова, 2011
Юрий Бедарев
Юрий Бедарев
Россия, Новосибирская область