Объектно-ориентированное программирование и программная инженерия

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

Разбить на страницы
Показывать лекцию целиком

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

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

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

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

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

    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 для вызова агента, не зная точно, какую программу он принесет.
  • Механизм имеет много различных применений. В данный момент проанализируем несколько наиболее важных.

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

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

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

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

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

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

    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 на заданном интервале:

    $$\int\limits_a^b f(x) dx$$

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

    your_integrator.integral(agent f, a, b)
    

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

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

    clock_tick.subscribe (agent some_routine)
    

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

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

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

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

    (рис 5.2) Список истории

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

    execute (agent r, agent r_inverse )
    

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

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

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

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

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

    Мир без агентов

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

    Можно ли вообще найти решение? Конечно, можно. Если то, что вам нужно, задается объектом, представляющим обертку действия, то достаточно создать этот объект привычным способом, задав нужный класс. Единственная преграда — пришлось бы создавать большое число новых классов. Давайте посмотрим, как эта идея будет работать на предыдущих примерах.

    Интегрирование типичный случай. При определении функции, выполняющей интегрирование, 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]. Как вы знаете, последовательность значений, заключенная в квадратные скобки, представляет манифестный кортеж В Eiffel манифестными называются неименованные константы, заданные своими значениями, такие, как 17 или "это константа". Тип манифестных констант однозначно определяется синтаксисом их записи. Манифестный кортеж [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]];
  • в последних двух категориях — 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 и других итераторов.

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

    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 — например, функции косинус (cosine), — но и для любой применимой функции.

    Вот пример реализации 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 для вызова агента, не зная точно, какую программу он принесет.
  • Механизм имеет много различных применений. В данный момент проанализируем несколько наиболее важных.

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

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

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

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

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

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

    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 на заданном интервале:

    $$\int\limits_a^b f(x) dx$$

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

    your_integrator.integral(agent f, a, b)
    

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

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

    clock_tick.subscribe (agent some_routine)
    

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

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

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

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

    (рис 5.2) Список истории

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

    execute (agent r, agent r_inverse )
    

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

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

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

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

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

    Мир без агентов

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

    Можно ли вообще найти решение? Конечно, можно. Если то, что вам нужно, задается объектом, представляющим обертку действия, то достаточно создать этот объект привычным способом, задав нужный класс. Единственная преграда — пришлось бы создавать большое число новых классов. Давайте посмотрим, как эта идея будет работать на предыдущих примерах.

    Интегрирование типичный случай. При определении функции, выполняющей интегрирование, 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]. Как вы знаете, последовательность значений, заключенная в квадратные скобки, представляет манифестный кортеж В Eiffel манифестными называются неименованные константы, заданные своими значениями, такие, как 17 или "это константа". Тип манифестных констант однозначно определяется синтаксисом их записи. Манифестный кортеж [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]];
  • в последних двух категориях — 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 и других итераторов.

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

    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 — например, функции косинус (cosine), — но и для любой применимой функции.

    Вот пример реализации 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 и его потомках. Без этого пришлось бы иметь два множества: одно для целей, другое — для аргументов.

    Вернуться к учебному плану