Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1989 / 61 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 6:

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

< Лекция 5 || Лекция 6: 1234 || Лекция 7 >

Типы агентов

В объявлении do_at_every_stop необходимо указать тип action, представляющий агента. Фактическое объявление выглядит так

do_at_every_stop (action: PROCEDURE[ANY, TUPLE [G]]
    ...Остальное как ранее [6]...]

Универсальный библиотечный класс PROCEDURE описывает агента — связанную с ним команду (процедуру). У класса два родовых параметра, представляющих типы, которые характеризуют процедуру p, связанную с агентом.

  • Первый обозначает класс, от которого приходит p, или предка этого класса. Так как ANY является предком всех классов, можно обычно использовать ANY, как это сделано здесь для do_at_every_stop, поскольку фактический параметр agent P, соответствующий action, не использует информацию о том, какому классу принадлежит P.
  • Второй аргумент — это всегда кортеж. Типы аргументов процедуры P должны быть согласованы с типами компонентов кортежа.

Параметр P должен быть процедурой, такой как print_stop_name или append_restaurants. У нее один аргумент типа G (родовой параметр LINEAR и его потомков, который также служит как тип для item и представляет тип элементов структуры данных). Как следствие, второй родовой параметр PROCEDURE должен быть TUPLE[G].

Выбор TUPLE[G] в качестве второго параметра — это то, что позволяет в теле do_at_every_stop вызывать P, какой бы она ни была, с правильными аргументами, используя такую запись из [5.6]

action.call ([item]
Листинг 5.11.

Фактически метод call объявлен в классе PROCEDURE, принимая аргумент типа OPEN — второй родовой параметр.

Класс PROCEDURE описывает агентов, связанных с командами. Он является частью иерархии из четырех классов в библиотеке KERNEL:

Классы агентов

Рис. 5.4. Классы агентов

Класс FUNCTION предназначен для агентов, обозначающих запросы, за исключением запросов, возвращающих результат типа BOOLEAN, последние покрываются классом PREDICATE. Класс FUNCTION имеет третий родовой параметр, задающий тип результата функции. Класс ROUTINE — отложенный класс покрывает все варианты агентов.

Приведу заголовки данных классов:

deferred class ROUTINE [BASE, OPEN -> TUPLE]
class PROCEDURE [BASE, OPEN -> TUPLE] inherit
  ROUTINE [ BASE, OPEN]
class FUNCTION [BASE, OPEN -> TUPLE, RES] inherit
  ROUTINE [ BASE, OPEN]
class PREDICATE [BASE, OPEN -> TUPLE] inherit
  FUNCTION [ BASE, OPEN, BOOLEAN ]

Второй параметр OPEN ограничен TUPLE, так что можно использовать только кортежный тип TUPLE[G] как фактический параметр. Выражение agentf в зависимости от природы f принадлежит одному из вышеприведенных типов — процедура, булевский запрос или запрос другого типа.

Для агентов, представляющих процедуры с двумя аргументами типов T и U, используйте

PROCEDURE [C, TUPLE [T, U ]]     — в качестве С - часто задается ANY

Для программ без аргументов второй фактический параметр будет просто TUPLE, как в PROCEDURE[ANY, TUPLE]/ Класс ROUTINE объявляет:

call (v: OPEN)
    - Вызов компонента с операндами, используя v для открытых операндов

Это позволяет вызывать агента, передавая подходящий кортеж, как показано выше [5.11]. Если здесь нет аргументов — фактический параметр для OPEN просто TUPLE, — call передается пустой кортеж.

Помимо прочего, FUNCTION и PREDICATE включают запрос:

last_result: RES
    — Возвращается результат последнего вызова call, если он есть.

Для удобства эти классы включают функцию item, комбинирующую вызовы call и last_result:

item (v: like open_operands): RES
    - Результат вызова компонента со всеми операндами, используя v для
    - открытых операндов.
    - (Будет вызывать call.)
  ensure
    set_by_call: Result = last_result

Таким образом f.item([x]) для агента f дает результат вызова ассоциированной функции с аргументом x.

Дом для фундаментальных итераторов

Класс LINEAR в EiffelBase — предок всех списковых классов, таких как LIST, LINKED_LIST и других, описывающих любую структуру, которую можно обойти линейно. Как таковой, он является естественным домом для множества компонент, представляющих итераторы:

  • do_all применяет некоторое действие поочередно ко всем элементам структуры, подобно do_at_every_stop ;
  • do_if применяется ко всем элементам, удовлетворяющим некоторому условию. Вариантами являются dowhile и dountil ;
  • for_all — тест проверки выполнения некоторого условия (представленного агентом) на всех элементах структуры. Аналогично exists проверяет выполнение условия хотя бы на одном элементе.

Аргументами являются:

  • в первых двух категориях — action, представляющее применяемое действие, типа PROCEDURE[ANY, TUPLE[G]];
  • в последних двух категориях — test, представляющий булевский запрос типа PREDICATE[ANY, TUPLE[G]].

Итераторы do_if, do_while и do_until имеют оба аргумента. В качестве примера использования рассмотрим некоторый класс, имеющий целочисленный атрибут sum и процедуру:

increase_sum (n: INTEGER)
      - Добавить n к sum
  do sum := sum + n
    ensure added: sum = old sum + n
  end 

Зададим список il:LIST[INTEGER]. Для этого списка после выполнения присваивания (sum:= 0) можно найти сумму всех элементов, вызвав:

il.do_all (agent increase_sum)

Написание итератора

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

Урок анатомии
Мы уже привыкли к анализу реального промышленного кода. Давайте проанализируем код do_all из класса LINEAR[G] библиотеки EiffelBase. Заодно разумно просмотреть код do_if и других итераторов.

Вот код, скопированный из библиотеки13 Комментарии, естественно, изменены

do_all (action: PROCEDURE [ANY, TUPLE[G]])
    - Применить action к каждому элементу.
    - Семантика не гарантируется, если action изменяет саму структуру;
    - В таких случаях применяйте итератор не к структуре, а к ее клону.
  local
    c: CURSOR
  do
    c := cursor
    from    - Основной цикл
      start
    until
      after
    loop
      action.call ([item])
      forth
    end
    go_to (c)
end

Мы можем проигнорировать операторы, связанные с курсором, и сосредоточиться на сигнатуре, комментариях и части, помеченной как "Основной цикл".

Процедура do_all принимает единственный аргумент — action, представляющий агента, который будет многократно выполняться. Его тип PROCEDURE[ANY, TUPLE[G]] указывает, что процедура, связанная с агентом, может приходить от произвольного класса (поскольку указан класс ANY ) и (поскольку указан TUPLE[G] ) должна принимать один аргумент типа G — формальный родовой параметр охватывающего класса.

Заголовочный комментарий предупреждает, что "семантика не гарантируется, если действие изменяет структуру". Предупреждение важно: может возникнуть хаос, если действие изменяет саму структуру, добавляя или удаляя, например, элементы (в упражнении вас попросят проверить такую ситуацию, но нужно иметь крепкие нервы). Вполне допустимо изменять содержимое элементов структуры в результате выполнения действия. Например, вполне безопасно использовать do_all для добавления 1 к каждому элементу списка целых:

 do_all ( agent increment)

Процедура increment изменяет элементы, но не модифицирует структуру:

increment(item:INTEGER) 
  — Увеличивает элемент на 1
do
  item = item + 1
end

Если требуется модифицировать структуру, то, как указано в комментарии, итерирование безопасно выполнять на клоне исходной структуры. Тогда действие action может модифицировать исходную структуру, не воздействуя на ее клон. Для клонирования структуры s достаточно выполнить s.cloned.

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

"Основной цикл" — сердце алгоритма — использует ту же схему, что и специальный случай [5.6]:

from start until after loop
  action.call ([item])
  forth
end
Листинг 5.12.

Эти пояснения помогают понять основные свойства do_all и подобных операций.

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

Прежде всего, хотя вы уже могли догадаться, главная оптимизация компилятора, являющаяся основой успеха приведенной схемы, связана с оператором, помеченным [5.12]. Манифестный кортеж, такой как [item], представляет объект класса TUPLE. После первой его передачи методу call объект более не нужен, но наивная реализация создавала бы его на каждом шаге цикла. Это плохо с позиций производительности, не только из-за проблем с памятью — в конечном итоге сборщик мусора утилизирует ненужные объекты, но из-за потерь времени, поскольку создание объекта — дорогая операция. Чтобы избежать потерь, следует, подобно человеку, который жить не может без чашечки кофе, но не использует бумажные одноразовые стаканчики, а приобретает красивую, удобную чашку, создать единственный объект для кортежа и повторно его использовать.

Эту оптимизацию можно запрограммировать явно, объявив соответствующую переменную t, создать до начала цикла кортеж [item], присвоить его t и передавать t вместо манифестного кортежа [item] как аргумент call.

На самом деле заботиться об этом не нужно, поскольку эту оптимизацию выполняет компилятор.

Почувствуй оптимизацию

Повторное использование кортежа

В схемах, таких как [5.12] для do_all, требующих создания многих кортежей, каждый из которых используется однократно, компилятор EiffelStudio генерирует код, который создает единственный кортеж и многократно его использует.

Последней рассматриваемой деталью кода do_all является переменная c, связанная с курсором. Ее назначение — гарантировать, что итератор оставляет структуру в том состоянии, в котором он ее нашел.

Список с курсором

Рис. 5.5. Список с курсором

Структуры LINEAR имеют курсор. Итерирование в do_all передвигает его, используя start и forth. Но другие части ПО также могут работать с этим списком и, передвинув курсор к определенной позиции, могут рассчитывать, что он там и окажется при следующем обращении к списку. Итераторы, такие как do_all, должны быть "законопослушными гражданами", они могут изменять положение курсора во время выполнения собственных операций, но в конце они должны возвратить его на прежнее место в позицию, представленную переменной c.

Экземпляр CURSOR является "внешним курсором", который представляет позицию в структуре, допускающей обход, но в отличие от "внутреннего курсора" хранится не в самой структуре, а как внешний объект. В данном случае мы используем внешний курсор для запоминания начальной позиции внутреннего курсора и восстановления позиции при выходе.

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

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