Расширение потребует рассмотрения операций так, как если бы они были объектами. С первого взгляда это противоречит базисной двойственности этих двух понятий.
Текстуальная структура наших ОО-программ также основана на этом различии: мы разделяем программы на классы, в основе каждого из которых лежит некоторый тип объектов. Каждая операция присоединяется к классу в виде программы — метода класса.
Кажется, что эти два понятия четко различаются: то, что программа может делать, — это операция, а действия выполняются над объектами.
И все же иногда интересно рассматривать операцию как объект или, более точно, определять объекты, чья единственная роль состоит в описании операции. Мы называем такие объекты агентами. В этой лекции они детально рассматриваются, но базисную идею изложить нетрудно прямо сейчас. Простого агента можно получить, используя нотацию
agent r
Это выражение, его значение — агент, представляющий программу r. Поскольку это выражение, то его можно присвоить переменной:
a := agent r
Здесь переменная a должна иметь подходящий тип.
Что мы можем делать с агентами? Понятно, что агент ассоциирован с компонентом (методом), поэтому одно из его использований состоит в вызове этого компонента. После вышеприведенного присваивания a обозначает агента, поэтому возможен вызов
a.call ([x, у])
Эффект от вызова такой же, как и от непосредственного вызова программы r для любых применимых x и у
r (x, у)
Метод call применим ко всем агентам, он принимает в качестве аргумента единственный кортеж, здесь [x, у]. Кортеж — это просто объект, представляющий последовательность значений (надеюсь, вы помните это, но если нет, то прежде чем продолжить чтение, перечитайте раздел 13.5).
Зачем использовать вызов метода call с агентом, как в [5.1], вместо прямого вызова [5.2]? Действительно, если известно, какую процедуру вы хотите вызвать, то пользоваться услугами агента нецелесообразно. Но теперь предположите, что a получено от другого программного элемента, например, как аргумент текущего метода. Тогда вы знаете только, что a обозначает некий метод (и, как мы увидим далее, какие типы аргументов у этого метода), но не знаем самого метода, в данном примере r.
Агенты дают нам возможность построить и поставлять объекты, представляющие операции, готовые к выполнению, с полным разделением между:
r, выполняя agentr ;call — любой точкой ПО, которая получает агента a и может применить call для вызова агента, не зная точно, какую программу он принесет.Механизм имеет много различных применений. В данный момент проанализируем несколько наиболее важных.
Еще одна область, где агенты играют важную роль, — это программирование, управляемое событиями: образец, известный под именем "Писатель-Читатели" или "Издатель-Подписчики", полезный, в частности, для реализации GUI (графического интерфейса пользователя), являющегося предметом следующей лекции.
Что нам предстоит изучить? Сравним агенты с другими приемами, основанными на ранее изученных механизмах, в частности, с динамическим связыванием, также применимым к некоторым из рассматриваемых применений агентов. Дадим краткий обзор математических основ — восхитительной теории
Для начала хорошо бы понять, зачем нам нужно рассматривать операции как объекты и что было бы в отсутствии такой возможности. Рассмотрим четыре примера: итерацию, интегрирование, наблюдение, откат.
Начнем с итерации. Нам приходилось использовать в циклах схему, где операция применяется к каждому элементу последовательной структуры, такой как список. Схема выглядит примерно так:
from start until after loop " Применить действие к элементу " forth end
Здесь 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 на заданном интервале:
Проблема в том, чтобы определить в классе 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 ([])
Опять-таки повторы выполняются не далее последнего элемента.
Нельзя понять по-настоящему агентов, если не задаться вопросом: а как пришлось бы действовать в случае отсутствия подобного механизма?
Можно ли вообще найти решение? Конечно, можно. Если то, что вам нужно, задается объектом, представляющим обертку действия, то достаточно создать этот объект привычным способом, задав нужный класс. Единственная преграда — пришлось бы создавать большое число новых классов. Давайте посмотрим, как эта идея будет работать на предыдущих примерах.
Интегрирование типичный случай. При определении функции, выполняющей интегрирование, integral,
введем аргумент, задающий подынтегральную функцию, типа INTEGRATABLE_FUNCTION. Соответствующий класс будет отложенным классом, который может выглядеть так:
note
description: " Функции, допускающие интегрирование на конечном интервале"
deferred class INTEGRATABLE_FUNCTION feature
item (x: REAL): REAL
— Значение функции в точке x.
Deferred
end
end
Можно спроектировать более изощренную форму этого класса, например, добавив запрос defined(x: REAL) и
использовать его в предусловии item, но этой простой версии достаточно для понимания архитектурных
проблем.
(рис 5.3) Классы для математических функций
Для каждой функции, которую необходимо проинтегрировать, необходимо, как показано на рисунке, создать небольшой
эффективный класс, такой как COSINE_FUNCTION, который обеспечивает требуемую реализацию отложенного
метода item:
- В классе COSINE_FUNCTION
item (x: REAL): REAL
- Значение функции в точке x.
do
Result := cosine (x)
end
Тогда для получения интеграла от функции cos(x) на интервале [a,b] необходимо объявить
переменную f:INTEGRATABLE_FUNCTION, убедиться, что динамически она присоединена к объекту типа
COSINE_FUNCTION, и вызвать
your_integrator.integral(f)
Функцию integral(f:INTEGRATABLE_FUNCTION) написать несложно, используя любой алгоритм численного
вычисления интегралов. Всякий раз, когда необходимо вычислить значение функции в некоторой точке x,
используется вызов f.item(x). Заметьте роль динамического связывания: тип f во время
выполнения, такой как COSINE_FUNCTION, определяет, какая подынтегральная функция (item)
будет использована. Чтобы убедиться, что вы понимаете схему и фундаментальные ОО-приемы программирования, —
хорошая идея написать integral самостоятельно.
Библиотека интегрирования без агентов
Напишите класс INTEGRATOR с методом integral, который вычисляет интеграл на конечном интервале
от функции, передаваемой в качестве аргумента типа INTEGRATABLE_FUNCTION. Спроектируйте подходящих потомков
INTEGRATABLE_FUNCTION, примените вашу работу для вычисления интегралов различных функций. В качестве простого
алгоритма интегрирования можно использовать модель, описанную ниже, для версии интегрирования, основанной на INTEGRATOR как
"программу с дырами". Вместо того чтобы вводить отдельный класс INTEGRATABLE_FUNCTlON, достаточно в
классе INTEGRATOR ввести отложенный метод integrated_function - аналог item.
Потомки класса будут переопределять эту функцию, задавая таким образом нужную им подынтегральную функцию. В этом варианте у
метода integral нет необходимости вводить аргумент f.
Этот пример типичен и демонстрирует преимущества стандартной ОО-техники, которую можно использовать, если не иметь агентов. Идеи достаточно просто распространяются и на остальные наши примеры.
ITERATABLE_ACTION, эффективные потомки которого обеспечат специфическую версию процедуры call, описывающей выполнение итерируемой операции.OBSERVER, потомки которого и обеспечивают свою версию метода update, вызываемую издателем при возникновении события. Хорошо известный образец "Наблюдатель" (OBSERVER) будет обсуждаться в следующей лекции.(Undo-Redo) для каждой команды интерактивной системы следует создать класс с
двумя методами: execute, выполняющий команду, и cancel, выполняющий откат, — устраняя
эффект последнего выполнения execute. Экземпляр этого класса описывает информацию, появляющуюся в
результате однократного выполнения команды, и необходимый откат, выполняемый в случае запроса. Например, в текстовом
редакторе экземпляр LINE_DELETION имеет два поля: содержимое строки перед удалением и позицию этой
строки в тексте, — так что метод cancel сможет восстановить строку, удаленную при выполнении команды
execute. Все такие командные классы наследуют от отложенного класса COMMAND, в котором
execute и cancel являются отложенными методами. Список истории может быть реализован,
например, как LINKED_LIST[COMMAND]. Это схема еще одного классического образца проектирования —
COMMAND.Мы можем называть эти приемы образцом проектирования "много маленьких оберток", поскольку здесь используется динамическое связывание, основанное на написании классов, типично небольших, представляющих обертку соответствующей операции.
Образец работает, но имеет очевидный недостаток, о чем свидетельствует данное ему имя, — наполнение ПО большим числом небольших классов. Вообще-то, в небольших классах нет ничего ошибочного. Но принципиально класс должен задавать важную абстракцию, так что кажется подозрительным класс, у которого только один важный компонент. Это подозрение усиливается наблюдением, что в двух приведенных примерах (интегрирование и итерирование) у классов кроме одного важного метода (item, call) нет атрибутов, а следовательно, нужен только один экземпляр класса, такой как экземпляр
COSINE_FUNCTION, присоединенный к f в [5.4]. Класс с одним экземпляром известен как "Одиночка" (Singleton). Но здесь объект не только присутствует в единственном экземпляре, но у него и полей вообще нет — странный объект, в самом деле. Каждый из классов представляет капсулу с одной единственной процедурой. Мы можем называть такие классы "певец одной песни".
Написание многих таких оберток усложняет ПО, в частности, структуру наследования, как мы увидим в следующей лекции при рассмотрении образца "Наблюдатель". В конце концов, это ужасно, что нельзя непосредственно использовать функцию cosinus для интегрирования или программу print_stop_name при обходе маршрута. Зачем нужно обертывать ее в одежды класса? В экстремальных случаях одна и та же математическая операция могла бы применяться для интегрирования, итерирования и наблюдения. Неужели ей нужны три разных наряда?
Среди наших примеров один из них — откаты и повторы — не кажется с этих позиций искусственным, поскольку абстракция COMMAND обоснована. У нее есть два важных метода — execute и cancel, так что у певца есть, по крайней мере, две песни, есть атрибуты, так что потомки описывают имеющие смысл различающиеся экземпляры.
В других наших примерах трудно согласиться с техникой "много маленьких оберток". Нам нужно обертывать операции, превращая их в объекты, но мы не хотим делать это самостоятельно. Лучше возложить это на некоторый механизм, встроенный в язык.
Таким механизмом являются агенты. Если f — это метод, то нетрудно получить в качестве подарка этот метод в красивых одеждах, просто написав agentf Мы получим объект, обладающий всем, что есть у f, включая возможность вызова f (используя call ) для любых применимых аргументов, который можно вызывать всюду и всякий раз, когда это понадобится. Во всех рассмотренных случаях и во многих других агенты являются соперниками образца "много маленьких оберток". Надеюсь, маленькое отступление — как обойтись без агентов — дает нам лучшее понимание преимуществ простого, встроенного механизма, позволяющего работать с действиями, как с объектами.
Теперь, когда мы видели, где полезны агенты и почему они нам необходимы, мы можем выйти за границы предыдущего рассмотрения и заняться полной картиной со всеми деталями. Данный раздел дополняет пример с итерированием, следующий — интегрированием. В следующей лекции будет представлено детальное описание образца "Наблюдатель", основанное на агентах.
Простой пример из Traffic иллюстрирует использование и определение итератора через агенты. Рассмотрим понятие маршрута ROUTE. Мы можем добавить в ROUTE метод do_at_every_stop, который принимает действие как аргумент и применяет его к каждой остановке. Это делает возможным вызовы:
your_route.do_at_every_stop (agent print_stop_name) your_route.do_at_every_stop (agent append_restaurants) ... your_route.do_at_every_stop (agent other_operation)
Предполагается, что print_stop_name, append_restaurants, other_operations являются методами, принимающими STOP в качестве аргумента.
Как должен выглядеть метод do_at_every_stop, чтобы все эти вызовы стали возможными? Он абстрагирует стандартную схему итерации, приведенную в этой лекции [3]:
do_at_every_stop (action :...)
— Применить действие к каждой остановке данного маршрута.
do
from start until after loop
action.call ([item]) [6]
forth
end
end
Для включения ассоциированного метода используется вызов call — процедура, доступная всем агентам,
чей эффект состоит в вызове метода агента с заданными аргументами; более точно, аргументом является единственный
кортеж, здесь [item]. Как вы знаете, последовательность значений, заключенная в квадратные скобки,
представляет манифестный
Так как предполагается, что action представляет метод, такой как print_stop_name, у которого один аргумент, кортеж, используемый здесь, [item], имеет ровно один элемент.
Эффект от вызова action.call([item]) точно такой же, как и при прямом вызове соответ ствующего метода, такого как
print_stop_name (item)
Это справедливо при условии, что аргумент, переданный do_at_every_stop, был agent
print_stop_name. Если же аргумент — agent append_restaurants, то это соответствует вызову
append_restaurants (item)
Разница с [5.6] в том, что внутри do_at_every_stop мы ничего не знаем, какой фактический метод задает action.
Эта техника является базисным механизмом итераторов в библиотеке EiffelBase. Далее мы, фактически, будем изучать библиотечную реализацию.
Интересное приложение итерирования состоит в непосредственной реализации механизмов исчисления предикатов:
кванторов "Для всех" (∀) и "Существует" (∃). Предположим, что вы хотите установить,
что все элементы массива целых a в границах a.lover и a.upper являются
положительными. В исчислении предикатов это выражается записью:
∀ s: a.lower .. a.upper | a [i] > 0
Без агентов вы могли бы использовать all_positive(a), написав функцию
all_positive(ia: ARRAY[INTEGER]), получающую результат, выполняя цикл. С агентами нет необходимости в такой функции, можно написать проще:
(a.lower|..|a.upper).for_all (agent is_positive))
Такая запись по стилю близка к предикатной записи [5.9]. Символика |..|
является операторным псевдонимом (alias) функции interval из класса INTEGER.
Результат выполнения данной функции принадлежит типу INTEGER_INTERVAL — библиотечному классу,
содержащему функции for_ all и exist. Данные конкретные функции for_ all и
exist применимы к массивам, но вскоре мы познакомимся с аналогичными функциями, применимыми к спискам и
другим последовательным структурам.
В [5.10] все же требуется проверка на положительность и запрос
is_positive(n:INTEGER): BOOLEAN. Это более разумно, чем требовать нечто подобное
all_positive для каждого такого случая. В конце лекции мы научимся, как избавиться даже от
is_positive, написав нужный агент без введения явной процедуры.
for_ all и exist, как они заданы в классе
INTEGER_INTERVAL. Пусть вас не смущает объявление типов (о них мы еще поговорим), но убедитесь, что вы
понимаете реализацию. Посмотрите также и на функцию exist1.В объявлении 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]
Фактически метод 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]];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 и других итераторов.Вот код, скопированный из
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
Эти пояснения помогают понять основные свойства 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 является "внешним курсором", который представляет позицию в структуре, допускающей обход, но в отличие от "внутреннего курсора" хранится не в самой структуре, а как внешний объект. В данном случае мы используем внешний курсор для запоминания начальной позиции внутреннего курсора и восстановления позиции при выходе.
Как показало рассмотрение итераторов, агенты позволяют нам описать операции, манипулирующие другими операциями. Такие же потребности часто возникают в вычислительной математике: вычисление интегралов — типичный пример.
Стандартная техника численного вычисления интеграла от вещественной функции f на конечном интервале
a...b состоит, как упоминалось в предыдущей лекции, в аппроксимации точного значения интеграла ФОРМУЛА!!!! конечной суммой площадей многих маленьких прямоугольников.
Если все эти прямоугольники имеют ширину step, то прямоугольник с координатами по оси абсцисс (x, x + step) имеет площадь step * f(x). Аппроксимация интеграла на заданном интервале является суммой
(рис 5.6) Численное вычисление интеграла
$$\sum\limits_{i=0}^{n-1} f(a+i*step)*step$$
всех площадей для всех x, таких, что a < x <. b. Число прямоугольников примерно равно (b — a)/ step
Агенты дают нам возможность написать функцию вычисления интеграла integral не только для конкретной подынтегральной функции f — например, функции косинус (, — но и для любой применимой функции.
Вот пример реализации integral:
integral (f : FUNCTION [ANY, TUPLE [REAL], REAL]; a, b : REAL): REAL
- Aппроксимация вычисления интеграла от функции f на интервале a..b
local
x: REAL ; i: INTEGER
do
from x := a until x >= b loop
Result := Result + f.item
i := i + 1; x := a + i * step
end
end
Объявление f указывает, что это функция с одним аргументом типа REAL, возвращающая значение типа REAL. Для вычисления значения функции в точке используем f.item ([x]). Ранее функция item для агентов уже была определена — она вызывает call и возвращает результат этого вызова. Эта функция ожидает кортеж в качестве аргумента, здесь ей передается манифестный кортеж.
Обратите внимание на вычисление заново х на каждом шаге вместо последовательного добавления шага. Это делается сознательно во избежание накопления ошибки.
Функция integral является частью класса INTEGRATOR, который описывает объекты,
отвечающие за интегрирование математических функций. Если your_integrator — объект этого типа, то можно получить интеграл от функции f на интервале a..b как значение
your_integrator. integral(agent f, a, b)
В классе INTEGRATOR следует объявить step как атрибут типа REAL со связанной сеттер-процедурой, что позволяет клиентам управлять точностью вычисления интеграла. Класс становится чем-то большим, чем простой оберткой функции, он определяет абстракцию, имеющую практический смысл — интегрирование с управляемой точностью.
Иногда при использовании агентов необходимо больше гибкости, поскольку нужно помимо аргументов, передаваемых ассоциированному методу при вызове, задать некоторые дополнительные аргументы, но сделать это надо один раз и на все время определения агента. Мы будем говорить о "закрытых" и "открытых" аргументах.
В качестве простого примера рассмотрим вариант последней схемы, где мы по-прежнему хотим вычислить интеграл от функции, но у функции есть дополнительные аргументы:
$$\int\limits_a^b g(u,x,v)dx$$Переменные u и v остаются константами во время интегрирования — только х, как и ранее, учитывается при интегрировании. Но значения переменных нужны для вычисления значения функции. Конечно, можно по-прежнему использовать предыдущее решение:
your_integrator.integral (agent g_extended, a, b
Необходимо определить функцию:
g_extended (x: REAL): REAL
— Функция та же, что и g, с первым и третьим аргументами, равными u и v
do
Result := g ( u, x, v)
end
Предполагается, что u и v являются атрибутами класса INTEGRATOR. Это работает, но писать такие функции утомительно, и особенно неприятно, если u и v являются локальными переменными или формальными аргументами.
Функции, такие как g_extended, просто обертка, чья единственная цель состоит в заморозке некоторых аргументов функции, превращая ее в функцию только оставшихся аргументов. Делать это приходится часто, поэтому стоит ввести подходящее понятие. Тот же эффект, что и в [5.13], можно получить без введения обертывающей функции, а используя выражение:
your_integrator.integral (agent g (u, ?, v), a, b)
Агентное выражение agent g(u, ?, v) обозначает функцию с одним аргументом, полученную из функции с тремя аргументами заморозкой первого и третьего аргумента значениями u и v соответственно. Истинным аргументом остается только аргумент во второй позиции, помеченный знаком вопроса.
Итак, при вызове агента задается список аргументов, соответствующий сигнатуре ассоциированной функции, но в этом списке любое число аргументов можно заменить знаком вопроса. Такие аргументы известны как открытые аргументы агента, а остальные, значения которых заданы при вызове, являются закрытыми аргументами агента. Агент рассматривает функцию только по отношению к открытым аргументам.
В частности, это означает, что наша первая нотация агента — agent f — является простым сокращением записи
agent f(?, ??, ...)
Здесь, у функции открыты все аргументы. В этом случае проще использовать краткую форму: agent f.
Понятие открытых аргументов увеличивает многогранность агентов, избавляя от необходимости задания дополнительных
функций — оберток, подобных g_extended. В качестве еще одного примера приведем вариацию схемы итерирования, включающей список целых. Рассмотрим программу:
increase_sum_by_power (?, n: INTEGER)
- Добавить к sum значение m в степени n.
do sum := sum + m^n ensure added: sum = old sum +m^n end
Теперь sum типа REAL, так как значение этого типа возвращается после возведения в степень. После присваивания sum:= 0.0 можно получить сумму квадратов всех элементов списка il, вызвав:
il.do_all (agent increase_sum_by_power (?, 2)
Обобщая:
"?".Терминологическое напоминание. "Определением агента" является выражение, его специфицирующее, такое как agent f(а, ?). Вызовом агента является оператор, вызывающий ассоциированный с агентом метод во время выполнения.
В последнем определении введен новый термин — операнд. До сих пор мы говорили об открытых аргументах. Зачем же понадобилось новое понятие? Причина в том, что иногда необходимо не только сохранять открытым аргумент, но открытой должна быть и цель вызова.
Рассмотрим снова прежний пример с маршрутами и остановками:
your_route.do_all (agent print_stop_name)
Вызов идентичен [5.5] за тем исключением, что в данном случае нет необходимости
в процедуре do_at_every_stop, мы можем непосредственно вызывать do_all, так как в
Traffic-класс ROUTE является фактически потомком LINEAR[STOP]. Пример предполагает
процедуру print_stop_name с сигнатурой
print_stop_name (s: STOP)
Появляющийся в классе C agent print_stop_name имеет тип PROCEDURE[C, TUPLE[STOP]], соответствуя типу формального аргумента do_all.
Процедура print_stop_name на экземпляры STOP смотрит извне — она не принадлежит этому классу, но имеет аргумент типа STOP. Этот аргумент и будет тем самым открытым аргументом, так как запись [5.14] в реальности является сокращением для
your_route.do_all (agent print_stop_name (?))
Всякий раз, когда в do_all вызывается метод call для агента, мы знаем, где это происходит: action. call[ item] для каждого item, представляющего STOP в маршруте. Эффект от этого вызова тот же, что и для прямого вызова ассоциированного метода:
print_stop_name (your_route.item)
Здесь подсветка в [5.15] и [5.16]
показывает, что передается в качестве аргумента итерируемому действию. Схема итерации
[5.14], основанная на агентах, эквивалентна циклу, явно использующему
your_route, инициализируя итерацию через your_route.start, продвигаясь по структуре
your_route.forth, и выполняя вызов [5.16] на каждом шаге.
Подобно любому вызову в ОО-программировании, этот вызов имеет цель, но здесь цель задана неявно — текущий
объект. Цель всегда можно сделать явной, записав вызов как Current.print_stop_name (your_route.item).
Предположим теперь, что мы рассматриваем не внешний, а внутренний метод класса STOP. Например, пусть в классе STOP существует метод close, отмечающий закрытые станции. Если мы хотим закрыть всю линию, то должны выполнить close для всех остановок. Но у метода close нет аргументов, он вызывается целью и применяется к ней; его типичный вызов:
some_stop.close
Так что действие, которое следует итерировать, теперь выглядит не как в [16], а так:
your_route.item.close
В данном случае целью метода, а не его аргументом, является то, что должно быть открытым в аргументе do_all и что должно заменить выражение agentprint_stop_name (?) в [5.15] (или в краткой форме [5.14]).
Первое, что приходит в голову, — это написать нечто подобное ? close. Но это не работает, так как не задан тип цели, ведь многие классы могут иметь компонент close.
Мы должны задать тип цели и правильной формой для нашего примера является:
your_route.do_all (agent {STOP}.close)
Теперь вы видите, почему необходимо ввести общий термин — операнд: он покрывает все значения, необходимые для выполнения вызова — цель и аргументы.
Для открытых аргументов применяется простая запись с использованием знака "?", поскольку тип аргумента можно выяснить из известной сигнатуры метода, но можно применять и запись с явным указанием типа TYPE. Это может быть полезно, если указывается тип, отличный от типа сигнатуры, но, естественно, согласованный с ним.
Все комбинации открытых и закрытых операндов являются правильными. Предположим, что f и g — методы класса C ; f имеет аргументы, а g — без аргументов. Тогда возможно:
agentf ( x, y, z ), agentg ;agentf ( ?, ?, ? ), agentg (возможна краткая форма записи в этом случае - agentf );agentf ( ?, y, ? );agent{C}f ( ?, y, ? );agent{C} ;Эти механизмы позволяют нам иметь единое множество итераторов — do_all, do_if и другие в LINEAR и его потомках. Без этого пришлось бы иметь два множества: одно для целей, другое — для аргументов.
Расширение потребует рассмотрения операций так, как если бы они были объектами. С первого взгляда это противоречит базисной двойственности этих двух понятий.
Текстуальная структура наших ОО-программ также основана на этом различии: мы разделяем программы на классы, в основе каждого из которых лежит некоторый тип объектов. Каждая операция присоединяется к классу в виде программы — метода класса.
Кажется, что эти два понятия четко различаются: то, что программа может делать, — это операция, а действия выполняются над объектами.
И все же иногда интересно рассматривать операцию как объект или, более точно, определять объекты, чья единственная роль состоит в описании операции. Мы называем такие объекты агентами. В этой лекции они детально рассматриваются, но базисную идею изложить нетрудно прямо сейчас. Простого агента можно получить, используя нотацию
agent r
Это выражение, его значение — агент, представляющий программу r. Поскольку это выражение, то его можно присвоить переменной:
a := agent r
Здесь переменная a должна иметь подходящий тип.
Что мы можем делать с агентами? Понятно, что агент ассоциирован с компонентом (методом), поэтому одно из его использований состоит в вызове этого компонента. После вышеприведенного присваивания a обозначает агента, поэтому возможен вызов
a.call ([x, у])
Эффект от вызова такой же, как и от непосредственного вызова программы r для любых применимых x и у
r (x, у)
Метод call применим ко всем агентам, он принимает в качестве аргумента единственный кортеж, здесь [x, у]. Кортеж — это просто объект, представляющий последовательность значений (надеюсь, вы помните это, но если нет, то прежде чем продолжить чтение, перечитайте раздел 13.5).
Зачем использовать вызов метода call с агентом, как в [5.1], вместо прямого вызова [5.2]? Действительно, если известно, какую процедуру вы хотите вызвать, то пользоваться услугами агента нецелесообразно. Но теперь предположите, что a получено от другого программного элемента, например, как аргумент текущего метода. Тогда вы знаете только, что a обозначает некий метод (и, как мы увидим далее, какие типы аргументов у этого метода), но не знаем самого метода, в данном примере r.
Агенты дают нам возможность построить и поставлять объекты, представляющие операции, готовые к выполнению, с полным разделением между:
r, выполняя agentr ;call — любой точкой ПО, которая получает агента a и может применить call для вызова агента, не зная точно, какую программу он принесет.Механизм имеет много различных применений. В данный момент проанализируем несколько наиболее важных.
Еще одна область, где агенты играют важную роль, — это программирование, управляемое событиями: образец, известный под именем "Писатель-Читатели" или "Издатель-Подписчики", полезный, в частности, для реализации GUI (графического интерфейса пользователя), являющегося предметом следующей лекции.
Что нам предстоит изучить? Сравним агенты с другими приемами, основанными на ранее изученных механизмах, в частности, с динамическим связыванием, также применимым к некоторым из рассматриваемых применений агентов. Дадим краткий обзор математических основ — восхитительной теории
Для начала хорошо бы понять, зачем нам нужно рассматривать операции как объекты и что было бы в отсутствии такой возможности. Рассмотрим четыре примера: итерацию, интегрирование, наблюдение, откат.
Начнем с итерации. Нам приходилось использовать в циклах схему, где операция применяется к каждому элементу последовательной структуры, такой как список. Схема выглядит примерно так:
from start until after loop " Применить действие к элементу " forth end
Здесь 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 на заданном интервале:
Проблема в том, чтобы определить в классе 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 ([])
Опять-таки повторы выполняются не далее последнего элемента.
Нельзя понять по-настоящему агентов, если не задаться вопросом: а как пришлось бы действовать в случае отсутствия подобного механизма?
Можно ли вообще найти решение? Конечно, можно. Если то, что вам нужно, задается объектом, представляющим обертку действия, то достаточно создать этот объект привычным способом, задав нужный класс. Единственная преграда — пришлось бы создавать большое число новых классов. Давайте посмотрим, как эта идея будет работать на предыдущих примерах.
Интегрирование типичный случай. При определении функции, выполняющей интегрирование, integral,
введем аргумент, задающий подынтегральную функцию, типа INTEGRATABLE_FUNCTION. Соответствующий класс будет отложенным классом, который может выглядеть так:
note
description: " Функции, допускающие интегрирование на конечном интервале"
deferred class INTEGRATABLE_FUNCTION feature
item (x: REAL): REAL
— Значение функции в точке x.
Deferred
end
end
Можно спроектировать более изощренную форму этого класса, например, добавив запрос defined(x: REAL) и
использовать его в предусловии item, но этой простой версии достаточно для понимания архитектурных
проблем.
(рис 5.3) Классы для математических функций
Для каждой функции, которую необходимо проинтегрировать, необходимо, как показано на рисунке, создать небольшой
эффективный класс, такой как COSINE_FUNCTION, который обеспечивает требуемую реализацию отложенного
метода item:
- В классе COSINE_FUNCTION
item (x: REAL): REAL
- Значение функции в точке x.
do
Result := cosine (x)
end
Тогда для получения интеграла от функции cos(x) на интервале [a,b] необходимо объявить
переменную f:INTEGRATABLE_FUNCTION, убедиться, что динамически она присоединена к объекту типа
COSINE_FUNCTION, и вызвать
your_integrator.integral(f)
Функцию integral(f:INTEGRATABLE_FUNCTION) написать несложно, используя любой алгоритм численного
вычисления интегралов. Всякий раз, когда необходимо вычислить значение функции в некоторой точке x,
используется вызов f.item(x). Заметьте роль динамического связывания: тип f во время
выполнения, такой как COSINE_FUNCTION, определяет, какая подынтегральная функция (item)
будет использована. Чтобы убедиться, что вы понимаете схему и фундаментальные ОО-приемы программирования, —
хорошая идея написать integral самостоятельно.
Библиотека интегрирования без агентов
Напишите класс INTEGRATOR с методом integral, который вычисляет интеграл на конечном интервале
от функции, передаваемой в качестве аргумента типа INTEGRATABLE_FUNCTION. Спроектируйте подходящих потомков
INTEGRATABLE_FUNCTION, примените вашу работу для вычисления интегралов различных функций. В качестве простого
алгоритма интегрирования можно использовать модель, описанную ниже, для версии интегрирования, основанной на INTEGRATOR как
"программу с дырами". Вместо того чтобы вводить отдельный класс INTEGRATABLE_FUNCTlON, достаточно в
классе INTEGRATOR ввести отложенный метод integrated_function - аналог item.
Потомки класса будут переопределять эту функцию, задавая таким образом нужную им подынтегральную функцию. В этом варианте у
метода integral нет необходимости вводить аргумент f.
Этот пример типичен и демонстрирует преимущества стандартной ОО-техники, которую можно использовать, если не иметь агентов. Идеи достаточно просто распространяются и на остальные наши примеры.
ITERATABLE_ACTION, эффективные потомки которого обеспечат специфическую версию процедуры call, описывающей выполнение итерируемой операции.OBSERVER, потомки которого и обеспечивают свою версию метода update, вызываемую издателем при возникновении события. Хорошо известный образец "Наблюдатель" (OBSERVER) будет обсуждаться в следующей лекции.(Undo-Redo) для каждой команды интерактивной системы следует создать класс с
двумя методами: execute, выполняющий команду, и cancel, выполняющий откат, — устраняя
эффект последнего выполнения execute. Экземпляр этого класса описывает информацию, появляющуюся в
результате однократного выполнения команды, и необходимый откат, выполняемый в случае запроса. Например, в текстовом
редакторе экземпляр LINE_DELETION имеет два поля: содержимое строки перед удалением и позицию этой
строки в тексте, — так что метод cancel сможет восстановить строку, удаленную при выполнении команды
execute. Все такие командные классы наследуют от отложенного класса COMMAND, в котором
execute и cancel являются отложенными методами. Список истории может быть реализован,
например, как LINKED_LIST[COMMAND]. Это схема еще одного классического образца проектирования —
COMMAND.Мы можем называть эти приемы образцом проектирования "много маленьких оберток", поскольку здесь используется динамическое связывание, основанное на написании классов, типично небольших, представляющих обертку соответствующей операции.
Образец работает, но имеет очевидный недостаток, о чем свидетельствует данное ему имя, — наполнение ПО большим числом небольших классов. Вообще-то, в небольших классах нет ничего ошибочного. Но принципиально класс должен задавать важную абстракцию, так что кажется подозрительным класс, у которого только один важный компонент. Это подозрение усиливается наблюдением, что в двух приведенных примерах (интегрирование и итерирование) у классов кроме одного важного метода (item, call) нет атрибутов, а следовательно, нужен только один экземпляр класса, такой как экземпляр
COSINE_FUNCTION, присоединенный к f в [5.4]. Класс с одним экземпляром известен как "Одиночка" (Singleton). Но здесь объект не только присутствует в единственном экземпляре, но у него и полей вообще нет — странный объект, в самом деле. Каждый из классов представляет капсулу с одной единственной процедурой. Мы можем называть такие классы "певец одной песни".
Написание многих таких оберток усложняет ПО, в частности, структуру наследования, как мы увидим в следующей лекции при рассмотрении образца "Наблюдатель". В конце концов, это ужасно, что нельзя непосредственно использовать функцию cosinus для интегрирования или программу print_stop_name при обходе маршрута. Зачем нужно обертывать ее в одежды класса? В экстремальных случаях одна и та же математическая операция могла бы применяться для интегрирования, итерирования и наблюдения. Неужели ей нужны три разных наряда?
Среди наших примеров один из них — откаты и повторы — не кажется с этих позиций искусственным, поскольку абстракция COMMAND обоснована. У нее есть два важных метода — execute и cancel, так что у певца есть, по крайней мере, две песни, есть атрибуты, так что потомки описывают имеющие смысл различающиеся экземпляры.
В других наших примерах трудно согласиться с техникой "много маленьких оберток". Нам нужно обертывать операции, превращая их в объекты, но мы не хотим делать это самостоятельно. Лучше возложить это на некоторый механизм, встроенный в язык.
Таким механизмом являются агенты. Если f — это метод, то нетрудно получить в качестве подарка этот метод в красивых одеждах, просто написав agentf Мы получим объект, обладающий всем, что есть у f, включая возможность вызова f (используя call ) для любых применимых аргументов, который можно вызывать всюду и всякий раз, когда это понадобится. Во всех рассмотренных случаях и во многих других агенты являются соперниками образца "много маленьких оберток". Надеюсь, маленькое отступление — как обойтись без агентов — дает нам лучшее понимание преимуществ простого, встроенного механизма, позволяющего работать с действиями, как с объектами.
Теперь, когда мы видели, где полезны агенты и почему они нам необходимы, мы можем выйти за границы предыдущего рассмотрения и заняться полной картиной со всеми деталями. Данный раздел дополняет пример с итерированием, следующий — интегрированием. В следующей лекции будет представлено детальное описание образца "Наблюдатель", основанное на агентах.
Простой пример из Traffic иллюстрирует использование и определение итератора через агенты. Рассмотрим понятие маршрута ROUTE. Мы можем добавить в ROUTE метод do_at_every_stop, который принимает действие как аргумент и применяет его к каждой остановке. Это делает возможным вызовы:
your_route.do_at_every_stop (agent print_stop_name) your_route.do_at_every_stop (agent append_restaurants) ... your_route.do_at_every_stop (agent other_operation)
Предполагается, что print_stop_name, append_restaurants, other_operations являются методами, принимающими STOP в качестве аргумента.
Как должен выглядеть метод do_at_every_stop, чтобы все эти вызовы стали возможными? Он абстрагирует стандартную схему итерации, приведенную в этой лекции [3]:
do_at_every_stop (action :...)
— Применить действие к каждой остановке данного маршрута.
do
from start until after loop
action.call ([item]) [6]
forth
end
end
Для включения ассоциированного метода используется вызов call — процедура, доступная всем агентам,
чей эффект состоит в вызове метода агента с заданными аргументами; более точно, аргументом является единственный
кортеж, здесь [item]. Как вы знаете, последовательность значений, заключенная в квадратные скобки,
представляет манифестный
Так как предполагается, что action представляет метод, такой как print_stop_name, у которого один аргумент, кортеж, используемый здесь, [item], имеет ровно один элемент.
Эффект от вызова action.call([item]) точно такой же, как и при прямом вызове соответ ствующего метода, такого как
print_stop_name (item)
Это справедливо при условии, что аргумент, переданный do_at_every_stop, был agent
print_stop_name. Если же аргумент — agent append_restaurants, то это соответствует вызову
append_restaurants (item)
Разница с [5.6] в том, что внутри do_at_every_stop мы ничего не знаем, какой фактический метод задает action.
Эта техника является базисным механизмом итераторов в библиотеке EiffelBase. Далее мы, фактически, будем изучать библиотечную реализацию.
Интересное приложение итерирования состоит в непосредственной реализации механизмов исчисления предикатов:
кванторов "Для всех" (∀) и "Существует" (∃). Предположим, что вы хотите установить,
что все элементы массива целых a в границах a.lover и a.upper являются
положительными. В исчислении предикатов это выражается записью:
∀ s: a.lower .. a.upper | a [i] > 0
Без агентов вы могли бы использовать all_positive(a), написав функцию
all_positive(ia: ARRAY[INTEGER]), получающую результат, выполняя цикл. С агентами нет необходимости в такой функции, можно написать проще:
(a.lower|..|a.upper).for_all (agent is_positive))
Такая запись по стилю близка к предикатной записи [5.9]. Символика |..|
является операторным псевдонимом (alias) функции interval из класса INTEGER.
Результат выполнения данной функции принадлежит типу INTEGER_INTERVAL — библиотечному классу,
содержащему функции for_ all и exist. Данные конкретные функции for_ all и
exist применимы к массивам, но вскоре мы познакомимся с аналогичными функциями, применимыми к спискам и
другим последовательным структурам.
В [5.10] все же требуется проверка на положительность и запрос
is_positive(n:INTEGER): BOOLEAN. Это более разумно, чем требовать нечто подобное
all_positive для каждого такого случая. В конце лекции мы научимся, как избавиться даже от
is_positive, написав нужный агент без введения явной процедуры.
for_ all и exist, как они заданы в классе
INTEGER_INTERVAL. Пусть вас не смущает объявление типов (о них мы еще поговорим), но убедитесь, что вы
понимаете реализацию. Посмотрите также и на функцию exist1.В объявлении 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]
Фактически метод 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]];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 и других итераторов.Вот код, скопированный из
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
Эти пояснения помогают понять основные свойства 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 является "внешним курсором", который представляет позицию в структуре, допускающей обход, но в отличие от "внутреннего курсора" хранится не в самой структуре, а как внешний объект. В данном случае мы используем внешний курсор для запоминания начальной позиции внутреннего курсора и восстановления позиции при выходе.
Как показало рассмотрение итераторов, агенты позволяют нам описать операции, манипулирующие другими операциями. Такие же потребности часто возникают в вычислительной математике: вычисление интегралов — типичный пример.
Стандартная техника численного вычисления интеграла от вещественной функции f на конечном интервале
a...b состоит, как упоминалось в предыдущей лекции, в аппроксимации точного значения интеграла ФОРМУЛА!!!! конечной суммой площадей многих маленьких прямоугольников.
Если все эти прямоугольники имеют ширину step, то прямоугольник с координатами по оси абсцисс (x, x + step) имеет площадь step * f(x). Аппроксимация интеграла на заданном интервале является суммой
(рис 5.6) Численное вычисление интеграла
$$\sum\limits_{i=0}^{n-1} f(a+i*step)*step$$
всех площадей для всех x, таких, что a < x <. b. Число прямоугольников примерно равно (b — a)/ step
Агенты дают нам возможность написать функцию вычисления интеграла integral не только для конкретной подынтегральной функции f — например, функции косинус (, — но и для любой применимой функции.
Вот пример реализации integral:
integral (f : FUNCTION [ANY, TUPLE [REAL], REAL]; a, b : REAL): REAL
- Aппроксимация вычисления интеграла от функции f на интервале a..b
local
x: REAL ; i: INTEGER
do
from x := a until x >= b loop
Result := Result + f.item
i := i + 1; x := a + i * step
end
end
Объявление f указывает, что это функция с одним аргументом типа REAL, возвращающая значение типа REAL. Для вычисления значения функции в точке используем f.item ([x]). Ранее функция item для агентов уже была определена — она вызывает call и возвращает результат этого вызова. Эта функция ожидает кортеж в качестве аргумента, здесь ей передается манифестный кортеж.
Обратите внимание на вычисление заново х на каждом шаге вместо последовательного добавления шага. Это делается сознательно во избежание накопления ошибки.
Функция integral является частью класса INTEGRATOR, который описывает объекты,
отвечающие за интегрирование математических функций. Если your_integrator — объект этого типа, то можно получить интеграл от функции f на интервале a..b как значение
your_integrator. integral(agent f, a, b)
В классе INTEGRATOR следует объявить step как атрибут типа REAL со связанной сеттер-процедурой, что позволяет клиентам управлять точностью вычисления интеграла. Класс становится чем-то большим, чем простой оберткой функции, он определяет абстракцию, имеющую практический смысл — интегрирование с управляемой точностью.
Иногда при использовании агентов необходимо больше гибкости, поскольку нужно помимо аргументов, передаваемых ассоциированному методу при вызове, задать некоторые дополнительные аргументы, но сделать это надо один раз и на все время определения агента. Мы будем говорить о "закрытых" и "открытых" аргументах.
В качестве простого примера рассмотрим вариант последней схемы, где мы по-прежнему хотим вычислить интеграл от функции, но у функции есть дополнительные аргументы:
$$\int\limits_a^b g(u,x,v)dx$$Переменные u и v остаются константами во время интегрирования — только х, как и ранее, учитывается при интегрировании. Но значения переменных нужны для вычисления значения функции. Конечно, можно по-прежнему использовать предыдущее решение:
your_integrator.integral (agent g_extended, a, b
Необходимо определить функцию:
g_extended (x: REAL): REAL
— Функция та же, что и g, с первым и третьим аргументами, равными u и v
do
Result := g ( u, x, v)
end
Предполагается, что u и v являются атрибутами класса INTEGRATOR. Это работает, но писать такие функции утомительно, и особенно неприятно, если u и v являются локальными переменными или формальными аргументами.
Функции, такие как g_extended, просто обертка, чья единственная цель состоит в заморозке некоторых аргументов функции, превращая ее в функцию только оставшихся аргументов. Делать это приходится часто, поэтому стоит ввести подходящее понятие. Тот же эффект, что и в [5.13], можно получить без введения обертывающей функции, а используя выражение:
your_integrator.integral (agent g (u, ?, v), a, b)
Агентное выражение agent g(u, ?, v) обозначает функцию с одним аргументом, полученную из функции с тремя аргументами заморозкой первого и третьего аргумента значениями u и v соответственно. Истинным аргументом остается только аргумент во второй позиции, помеченный знаком вопроса.
Итак, при вызове агента задается список аргументов, соответствующий сигнатуре ассоциированной функции, но в этом списке любое число аргументов можно заменить знаком вопроса. Такие аргументы известны как открытые аргументы агента, а остальные, значения которых заданы при вызове, являются закрытыми аргументами агента. Агент рассматривает функцию только по отношению к открытым аргументам.
В частности, это означает, что наша первая нотация агента — agent f — является простым сокращением записи
agent f(?, ??, ...)
Здесь, у функции открыты все аргументы. В этом случае проще использовать краткую форму: agent f.
Понятие открытых аргументов увеличивает многогранность агентов, избавляя от необходимости задания дополнительных
функций — оберток, подобных g_extended. В качестве еще одного примера приведем вариацию схемы итерирования, включающей список целых. Рассмотрим программу:
increase_sum_by_power (?, n: INTEGER)
- Добавить к sum значение m в степени n.
do sum := sum + m^n ensure added: sum = old sum +m^n end
Теперь sum типа REAL, так как значение этого типа возвращается после возведения в степень. После присваивания sum:= 0.0 можно получить сумму квадратов всех элементов списка il, вызвав:
il.do_all (agent increase_sum_by_power (?, 2)
Обобщая:
"?".Терминологическое напоминание. "Определением агента" является выражение, его специфицирующее, такое как agent f(а, ?). Вызовом агента является оператор, вызывающий ассоциированный с агентом метод во время выполнения.
В последнем определении введен новый термин — операнд. До сих пор мы говорили об открытых аргументах. Зачем же понадобилось новое понятие? Причина в том, что иногда необходимо не только сохранять открытым аргумент, но открытой должна быть и цель вызова.
Рассмотрим снова прежний пример с маршрутами и остановками:
your_route.do_all (agent print_stop_name)
Вызов идентичен [5.5] за тем исключением, что в данном случае нет необходимости
в процедуре do_at_every_stop, мы можем непосредственно вызывать do_all, так как в
Traffic-класс ROUTE является фактически потомком LINEAR[STOP]. Пример предполагает
процедуру print_stop_name с сигнатурой
print_stop_name (s: STOP)
Появляющийся в классе C agent print_stop_name имеет тип PROCEDURE[C, TUPLE[STOP]], соответствуя типу формального аргумента do_all.
Процедура print_stop_name на экземпляры STOP смотрит извне — она не принадлежит этому классу, но имеет аргумент типа STOP. Этот аргумент и будет тем самым открытым аргументом, так как запись [5.14] в реальности является сокращением для
your_route.do_all (agent print_stop_name (?))
Всякий раз, когда в do_all вызывается метод call для агента, мы знаем, где это происходит: action. call[ item] для каждого item, представляющего STOP в маршруте. Эффект от этого вызова тот же, что и для прямого вызова ассоциированного метода:
print_stop_name (your_route.item)
Здесь подсветка в [5.15] и [5.16]
показывает, что передается в качестве аргумента итерируемому действию. Схема итерации
[5.14], основанная на агентах, эквивалентна циклу, явно использующему
your_route, инициализируя итерацию через your_route.start, продвигаясь по структуре
your_route.forth, и выполняя вызов [5.16] на каждом шаге.
Подобно любому вызову в ОО-программировании, этот вызов имеет цель, но здесь цель задана неявно — текущий
объект. Цель всегда можно сделать явной, записав вызов как Current.print_stop_name (your_route.item).
Предположим теперь, что мы рассматриваем не внешний, а внутренний метод класса STOP. Например, пусть в классе STOP существует метод close, отмечающий закрытые станции. Если мы хотим закрыть всю линию, то должны выполнить close для всех остановок. Но у метода close нет аргументов, он вызывается целью и применяется к ней; его типичный вызов:
some_stop.close
Так что действие, которое следует итерировать, теперь выглядит не как в [16], а так:
your_route.item.close
В данном случае целью метода, а не его аргументом, является то, что должно быть открытым в аргументе do_all и что должно заменить выражение agentprint_stop_name (?) в [5.15] (или в краткой форме [5.14]).
Первое, что приходит в голову, — это написать нечто подобное ? close. Но это не работает, так как не задан тип цели, ведь многие классы могут иметь компонент close.
Мы должны задать тип цели и правильной формой для нашего примера является:
your_route.do_all (agent {STOP}.close)
Теперь вы видите, почему необходимо ввести общий термин — операнд: он покрывает все значения, необходимые для выполнения вызова — цель и аргументы.
Для открытых аргументов применяется простая запись с использованием знака "?", поскольку тип аргумента можно выяснить из известной сигнатуры метода, но можно применять и запись с явным указанием типа TYPE. Это может быть полезно, если указывается тип, отличный от типа сигнатуры, но, естественно, согласованный с ним.
Все комбинации открытых и закрытых операндов являются правильными. Предположим, что f и g — методы класса C ; f имеет аргументы, а g — без аргументов. Тогда возможно:
agentf ( x, y, z ), agentg ;agentf ( ?, ?, ? ), agentg (возможна краткая форма записи в этом случае - agentf );agentf ( ?, y, ? );agent{C}f ( ?, y, ? );agent{C} ;Эти механизмы позволяют нам иметь единое множество итераторов — do_all, do_if и другие в LINEAR и его потомках. Без этого пришлось бы иметь два множества: одно для целей, другое — для аргументов.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.