Объекты и полиморфизм. Ч.1
Объекты и полиморфизм |
---|
Оригинал Objects and Polymorphism
Перевод выполнен участником форума ProgZ c ником Krys
Редакция Victor Yukhtenko
Язык программирования VIP подвергся в последние годы существенной переработке и есть намерения развивать его дальше. Наиболее заметен перенос акцентов на объектные системы и введение параметрического полиморфизма. В этой статье мы объясним причины включение в язык этих характеристик. Для иллюстрации будут приведены примеры того, как эти средства помогают решить проблему увеличения сложности и размера программного обеспечения.
Мотивация
VIP имеет много достоинств. Одним из главных является то, что он происходит из бронзового века IT-технологии, и глупо думать, что он останется современным после более, чем трех десятилетий, особенно с точки зрения скорости, которая с тех пор значительно выросла. Уже тогда вы могли написать изумительную программу, которая описывала, как фермеру перевезти через реку капусту, козла и волка на маленькой лодке так, чтобы никто никого не съел. Вы все еще можете писать такие программы также легко, но они больше никого не изумляют. Покажите ее вашим детям, и они немедленно спросят вас, почему решение не анимировано и объяснят, что имеется миллион более продвинутых программ, для этого достаточно обратиться в Интернет. Сегодня программы должны быть более совершенными, чем вчера. Они должны жить и взаимодействовать с окружающим миром гораздо теснее, чем вчера, и это непрерывный процесс. На эти вызовы было сделано много попыток ответить. Мы выбрали элементы, которые, как полагаем, в наибольшей степени улучшат характеристики VIP. В первую очередь, это объектная ориентация и параметрический полиморфизм, но имеются и другие новые элементы, из мира языков программирования.
Цели и средства
Целью модернизации языка является улучшение средств создания и обслуживания более сложных программ в более сложных областях.
Упрощения
Человек стремится иметь дело с простыми вещами, поэтому он сводит сложности к чему-нибудь простому, чаще всего используя понятие структуры. Главным принципом является «разделяй и властвуй»: деление первоначальной проблемы на отдельные подпроблемы, которые затем решаются по отдельности. Т.е., решение общей проблемы состоит из ряда промежуточных решений с последующим их объединением.
Удобство сопровождения
Для программного обеспечения часто недостаточно решить проблему, мы должны иметь возможность сопровождать и развивать программу в меняющимся мире. Поэтому очень важно, чтобы используемые методы не только решали задачу, но также приводили к программе, которую можно было бы сопровождать. Важно, чтобы программа была понятной, а этап деления на части общей проблемы в программе был видимым и ясным.
Повторное использование и распределение
После разделения общей задачи на составляющие, мы будем часто встречаться с одинаковыми подзадачами. В этих случаях было бы удобным сократить процесс повторного решения одинаковых подзадач. Т.е., эти решения следует попытаться адаптировать к новым контекстам, что можно сделать двумя путями. Можно скопировать и отредактировать подзадачу для работы в новых условиях, или можно попытаться приспособить единый программный код для решения нескольких задач. Очевидно, что последнее самое трудное, и имеет смысл только в том случае, если все части программного обеспечения можно будет сопровождать. В этом случае мы получаем преимущество, сопровождать код в одном контексте, одновременно, распределяя результат по другим контекстам.
Гибкость
Желательно, чтобы программный код был гибким. Например, можно передать заказчикам несколько вариантов программного обеспечения. Или может возникнуть необходимость изменить конфигурацию программы в изменившихся условиях. Или потребуется распределить решения подзадач в не полностью совместимых контекстах.
Мощность
И, наконец, желательно, чтобы расширение языка сопровождалось увеличением его мощности. Например, путем упрощения кода, необходимого для выполнения стандартных правил языка.
Полиморфизм
VIP расширен многими языковыми возможностями для поддержки достижения цели, о чем говорилось выше, но нужен программист, который использовал бы эти возможности. Теперь мы поговорим о том, как понимать, и как использовать полиморфизм. Объектная система VIP дает возможность использования, так называемого, категорированного полиморфизма (subsumption polymorphism), но VIP предоставляет еще и параметрический полиморфизм. Полиморфизм особенно пригоден для реализации принципа «разделяй и властвуй» тем, что поддерживает совместное испльзование (sharing) кода.
Категорированный полиморфизм
Объект является скрытой сущностью, которая характеризуется состоянием объекта и которая содержит некоторый код. Объект имеет интерфейс, с помощью которого можно взаимодействовать с объектом. Объект через свой интерфейс может взаимодействовать с другими объектами. Каждый интерфейс имеет определение в виде исходного кода. Определение, фактически, состоит из имени и объявления нескольких предикатов. Представим, что нам нужно создать объект, с помощью которого некоторая административная программа может выдать отчет о зарплатах работников. В очень упрощенном виде такой объект может иметь следующий интерфейс:
interface salarySystem predicates salary : (string Name, real Amount). end interface salarySystem
Объект с таким интерфейсом позволит выдать отчет по величине зарплаты Amount для персон с идентификатором Name.
Интерфейс salarySystem определяет тип данных, который можно использовать в программе. Главный предикат, необходимый для генерации отчета о зарплатах можно объявить примерно так:
class predicates reportSalary : (salarySystem SalarySystem).
Для примера, упростим программу выдачи отчета так:
clauses reportSalary(SalarySystem) :- SalarySystem:salary("John D", 135.12), SalarySystem:salary("Elvis P", 117.00).
Теперь нужно заполнить раздел implement объекта в нашей программе для зарплат. Именно здесь категорированный полиморфизм вступает в игру. Имеется особенность, состоящая в том, что заказчики программы использует различные системы обработки зарплат, каждая из которых получает на входе данные в различных форматах и с разных устройств.
Объекты являются реализациями классов, а классы, к счастью, могут пользоваться одним и тем же интерфейсом. Объекты, созданные любым из этих классов, могут быть использованы предикатом reportSytem . Т.е. предикат reportSytem должен быть полиморфным, т.к. тип salarySystem категорирует все варианты использования одного и того же интерфейса. Для работы с системой зарплат - ACME - объявим следующий класс:
class acme : salarySystem end class acme
Это объявление просто говорит, что acme является классом, создающим объекты типа salarySystem. Это класс нуждается в разделе implement:
implement acme constants fmt = "SAL:%>>>%\n". clauses salary(Name, Amount) :- stdio::writef(fmt, Name, Amount). end implement acme
Для иллюстрации полиморфизма, объединимся с системой зарплат OSalery. Для которой вновь, объявим разделы class и implement:
class oSalary : salarySystem end class oSalary implement oSalary constants fmt = "<salary name=\"%\" wage=\"%\" />\n". clauses salary(Name, Amount) :- stdio::writef(fmt, Name, Amount). end implement oSalary
Теперь испытаем нашу объединенную систему по обработке зарплат:
clauses test():- ACME = acme::new(), reportSalary(ACME), Osalery = oSalary::new(), reportSalary(Osalery).
В предикате test создаются объекты для каждой из двух систем, и генерируется отчет о зарплатах:
SAL:John D>>>135.12 SAL:Elvis P>>>117 <salary name="John D" wage="135.12" /> <salary name="Elvis P" wage="117" />
В этом примере отличия двух объектов не столь велики, но можно разместить результат в базе данных, используя другие языки программирования, обратиться к WEB, или еще куда-нибудь.
В этом примере используется принцип «разделяй и властвуй» для разделения программы генерации отчетов на независимую часть и на специфическую часть. Независимая часть отвечает за генерацию отчетов на уровне компаний, а зависимая часть работает только на уровне персон.
Мы использовали категорированный полиморфизм для гибкости при работе с несколькими системами по обработке зарплат. В реальности сделать это не так просто. Залогом успеха служит то, что интерфейс salarySystem является достаточно мощным, абстрактным и обобщенным для работы с подходящими системами. Например, может оказаться необходимым иметь несколько способов идентификации персон в предикате salary. Тогда системы зарплат могут использовать свои методы идентификации и игнорировать другие.
Предикат reportSalary распределен по нескольким системам зарплат. Распределение является высокоуровневой областью доступа для одного или нескольких приложений.
VIP имеет еще один вид категорированного полиморфизма: предикатные значения. Предикатным значением является предикат, который назначен переменной, передающийся, как параметр и который можно сохранить в фактах. Еще раз, полиморфизм имеет место при наличия типа, который категорирует различные применения.
Например, библиотека PFС использует предикатные значения в своей широко применяемой схеме уведомления событий (event notification scheme). Такая схема может, конечно, использоваться и помимо PFC.
Схема уведомления событий объединяет источник события и обработчик события. Имеется один источник события, но могут существовать несколько обработчиков события. Источник события предоставляет предикаты для зарегистрированных и незарегистрированных обработчиков событий. Если событие происходит, то зарегистрированные обработчики уведомляются.
Интересно отметить, что обработчик события является некоторым предикатом, следовательно, уведомление приведет к немедленному исполнению кода. Кроме того, вследствие категорированного полиморфизма, каждый из обработчиков может иметь свой собственный раздел применения и, таким образом, исполнять совершенно разные действия.
Очень важным свойством предикатных значений является то, что они могут быть объектными предикатами и иметь доступ к состоянию объектов, к которым они принадлежат.
Мы не будем здесь описывать в деталях схему уведомления событий, но знать ее надо. Читателю рекомендуется изучить концепцию классов PFC (поищите addXxxxListener). Использование предикатных значений будет изучаться на примере планирования работ в конце статьи.
Параметрический полиморфизм
В VIP введен параметрический полиморфизм. Подобно категорированному полиморфизму, целью параметрического полиморфизма является разнообразное использование одного и того же кода. Однако, если категорированный полиморфизм служит для поддержки разных применений в одном и том же контексте, то параметрический полиморфизм служит для использования одного и того же применения в разных контекстах.
В качестве введения, начнем с очень простого примера. Применим некоторую функцию, которая принимает список в качестве аргумента и по очереди возвращает (нон-детерминированно) элементы списка.
В VIP списочный домен является полиморфным доменом: если Elem есть тип (домен), то Elem* является списочным доменом. Новое состоит в том, что параметры, такие, как Elem и типовые выражения, такие, как Elem*, могут использоваться в объявлении, и такое объявление обслуживает любой вариант реализации параметров.
Объявим фуекцию getMamber_nd:
predicates getMember_nd : (Elem* List) -> Elem Value nondeterm.
В этом объявлении говорится, что getMamber_nd является функцией, которая принимает в качестве аргумента список List типа Elem* и возвращает значение Value типа Elem, где Elem любой тип.
Без дальнейших напоминаний, getMamber_nd может использоваться для любых списков:
clauses run():- console::init(), L = [[1,2,3], [7,-7], [9,4,5]], foreach X = list::getMember_nd(L) do stdio::writef("X = %\n", X) end foreach.
Здесь Elem есть integer* (т.е. список целых чисел). Печать выглядит так:
X = [1,2,3] X = [7,-7] X = [9,4,5]
Списочный домен, это предопределенный полиморфный домен. Но, можно определить полиморфный домен и самому. Для примера, можно создать «очередь с приоритетом», которую иногда называют «кучей» (heap). Пример, приведенный здесь не очень эффективен, зато он простой.
Идея очереди с приоритетом состоит в том, что данные (Data), которые вставляются в очередь, имеют приоритет (Priority). Позже, можно извлечь данные с самым низким значением Priority (чем ниже значение Priority, тем выше приоритет).
Очередь должна представлять собой список кортежей (tuple), каждый кортеж в нашем случае состоит из двух элементов Priority и Data. Список будет всегда находиться в отсортированном виде, где элемент с наименьшим значением Priority будет первым. tuple в VIP уже определен и выглядит так:
domains tuple{T1, T2} = tuple(T1, T2).
Существуют кортежи с другой арностью, но сейчас нам нужны только пары. Это пример полиморфного домена, заданного программистом. Определение говорит о том, что tuple это домен с двумя параметрами Т1 и Т2, который представляет собой многочисленную семью доменов, соответствующих всем реализациям типов параметров.
domains tuple_char_char = tuple(char, char). tuple_char_integer = tuple(char, integer). tuple_char_string = tuple(char, string).
Однострочное определение домена (см. выше) соответствует всем этим доменам. Фактически домены выглядят так:
tuple{char, char} tuple{char, integer} tuple{char, string} &
Значения выглядят так, как и ожидалось. Рассмотрим более сложный пример:
X = tuple(tuple('a',"a"), [1.2])
X есть tuple, его первый элемент есть опять tuple (содержащий char и string), второй элемент является списком чисел real. Следовательно, Х имеет тип:
tuple{ tuple{char, string}, real* }
Типовые выражения выглядят лучше нескольких строк. Возможно, вскоре, вы начнете все это применять. Ниже будет приведена небольшая схема, которая поможет вам делать типовые выражения более простыми и понятными. Эта схема, в то же время, повысит безопасность при использовании типов.
Теперь мы готовы определить домен очереди:
domains queue_rep{Priority, Data} = tuple{Priority, Data}*.
Суффикс _rep в имени домена относится к упомянутой выше схеме. Сейчас не будем обращать на него внимание. Определение домена говорит, что queue_rep является доменом с двумя параметрами Priority и Data. Более того, он является списком tuple этих типов.
Вы можете удивиться тому, что Priority является некоторым типовым параметром, ведь вы, наверное, думаете, что он должен иметь числовой тип, например, integer. Однако, в VIP типы данных упорядочены, и поэтому любой тип данных может быть использован в качестве приоритета. Использование здесь типового параметра делает обработку очереди более гибкой и не нужно выбирать тип приоритета: integer, unsigned или real.
Для того, чтобы узнать, следует ли использовать типовый параметр, или обычный тип, нужно понять, что вам нужно от этого типа? Например, сейчас нам нужно упорядочение (сравнение).
Отметим следующие возможности типовых параметров:
- передача в качестве параметра
- назначение и унификация (со значениями того же типа)
- сравнение (со значениями того же типа)
- запись в потоки
- чтение из потоков
Последнее частично неверно, компилятор позволяет чтение, но на практике нельзя прочитать все виды данных. Например, невозможно прочитать из потоков значения предикатов и объектов.
Вернемся к нашей очереди с приоритетами. Поскольку домен, представляющий очередь уже определен, нам нужно объявить и заполнить раздел implement подходящей работой.
Начнем с простого. Сначала нужно выполнить действия по получению элемента с самым маленьким значением приоритета. Для этого сделаем следующее объявление:
predicates tryGetLeast_rep : (queue_rep{Priority, Data} Queue) -> tuple{Priority, Data} Least determ.
Этот предикат детерминирован, поскольку очередь может быть пустой. Предикат является функцией, чей аргумент является очередью с приоритетами, и которая возвращает кортеж (tuple), содержащий и Priority, и Data. Предикат элементы из очереди не удаляет, т.к. мы хотим иметь возможность включение элементов в очередь без их удаления.
Т.к. список сортируется, начиная с наименьшего значения приоритета, то раздел implement очевиден:
clauses tryGetLeast_rep([Least|_]) = Least.
Аналогично, нужен предикат для удаления наименьшего элемента:
predicates deleteLeast_rep : (queue_rep{Priority, Data} Queue1) -> queue_rep{Priority, Data} Queue. clauses deleteLeast_rep([]) = []. deleteLeast_rep([_Least|Rest]) = Rest.
Наконец, нужно иметь предикат для вставки:
predicates insert_rep : (queue_rep{Pri, Data} Q1, Pri P, Data D) -> queue_rep{Pri, Data} Q0. clauses insert_rep([], Pri, Data) = [tuple(Pri, Data)]. insert_rep(Q1, Pri, Data) = Q0 :- [tuple(P1,D1)|Rest] = Q1, if Pri <= P1 then Q0 = [tuple(Pri, Data)| Q1] else Q0 = [tuple(P1,D1) | insert_rep(Rest, Pri, Data)] end if.
Типовые переменные вроде Pri имеют диапазон видимости только в пределах фрагмента, где они объявлены и определены. Поэтому нет ошибки при использовании имени Priority в одном объявлении, а Pri в другом. Тем не менее, нужно тщательно выбирать имена переменных, поскольку хорошее имя делает программу яснее. Компилятор не напомнит вам, что вы ошибочно заменили Priority на Data, хотя у многих программистов бывала путаница.
Такая очередь с приоритетами может использоваться по разному, но она имеет два недостатка, оба из которых имеют отношение к самому способу определения домена очереди:
domains queue_rep{Priority, Data} = tuple{Priority, Data}*.
Это объявление является, просто, сокращением или синонимом, т.е. queue_rep{integer, sting} это то же самое, что tuple{integer, string}*.
- Дебагер будет всегда показывать открытый тип (т.е. tuple{integer, string}*), что может слегка запутать.
- Любая часть данных, которая имеет этот тип, может быть случайно задана в виде аргумента предикату очереди с приоритетами. Но, для работы с такими очередями требуется не только, чтобы данные имели нужный тип, но и, чтобы очередь была отсортирована специальным образом (т.е., чтобы данные были независимыми).
Последняя проблема может возникать и без полиморфизма. Она происходит из-за использования обычных структур данных в специфических условиях.
Взамен, мы рассматриваем домен queue_rep, как внутреннее представление (representation) очереди, отсюда и расширение _rep. Для внешнего мира мы будем сворачивать каждую очередь queue_rep в функтор.
Реальный домен очереди будет выглядеть так:
domains queue{Priority, Data} = queue(queue_rep{Priority, Data}).
Такой домен уже не является ни аббревиатурой, ни синонимом. Экспортируемые, public предикаты будут, конечно, работать с queue, поэтому полностью объявление класса выглядит так:
class priorityQueue open core domains queue_rep{Priority, Data} = tuple{Priority, Data}*. domains queue{Priority, Data} = queue(queue_rep{Priority, Data}). constants empty : queue{Priority, Data} = queue([]). predicates insert : (queue{Pri, Data} Q1, Pri Pri, Data Data)-> queue{Pri, Data} Q0. predicates tryGetLeast : (queue{Priority, Data} Queue)-> tuple{Priority, Data} Least determ. predicates deleteLeast : (queue{Priority, Data} Queue1)-> queue{Priority, Data} Queue. end class priorityQueue
Мы добавили в класс константу для пустой очереди. Теперь, будучи пользователями этого класса, вам вообще не придется иметь дело с представлением очередей, поскольку для этого есть предикаты и константы.
Предикаты этого класса очень просты к применению. Единственно, что они должны делать, это удалять и добавлять функтор очереди в нужное место.
clauses insert(queue(Queue), Priority, Data) = queue(insert_rep(Queue, Priority, Data)). clauses tryGetLeast(queue(Queue)) = tryGetLeast_rep(Queue). clauses deleteLeast(queue(Queue)) = queue(deleteLeast_rep(Queue)).
Использование схемы _rep слегка снижает эффективность, из-за дополнительного функтора.
Применять ее или нет, зависит всецело от вашего темперамента. В любом случае использование _rep делает ваш тип уникальным, и он перестает быть синонимом.
Очереди с приоритетами могут быть использованы так:
clauses test():- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq2, 12, "Hello"), Pq4 = priorityQueue::insert(Pq3, 23, "!"), stdio::writef("%\n", Pq4).
Печать выглядит так:
queue( [tuple(12,"Hello"), tuple(17,"World"), tuple(23,"!")]) <vip> Может показаться, что полиморфизм препятствует контролю типов, но, в действительности, он дает возможность осуществлять строгий и гибкий контроль. Например, мы пишем: <vip> clauses test():- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq2, 12, 13).
И получаем сообщение об ошибке:
error c504: The expression has type '::integer', which is incompatible with the type '::string'
Первый insert присваивает тип string типу Data предиката Pq2, поэтому в следующей строке использовать тип integer уже нельзя.
Может показаться более удивительным то, что типу Data предиката Pq1 тоже присваивается тип string. Поэтому, следующий код порождает ту же ошибку:
clauses test():- Pq1 = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1, 17, "World"), Pq3 = priorityQueue::insert(Pq1, 12, 13).
С другой стороны, этот код правильный:
clauses test():- Pq1a = priorityQueue::empty, Pq2 = priorityQueue::insert(Pq1a, 17, "World"), Pq1b = priorityQueue::empty, Pq3 = priorityQueue::insert(Pq1b, 12, 13).
Мораль состоит в том, что константа empty является полиморфной, а переменные могут иметь только мономофные типы, поэтому, когда empty назначается переменной, обычный тип «замораживается».
Очереди с приоритетами могут использоваться для многих целей. И код может быть распределен, как в пределах одной программы, так и между различными программами. Параметрический полиморфизм позволяет использовать для решения разных проблем один способ, например, очередь с приоритетами.
Категорированный полиморфизм является в известной степени дополнением к параметрическому полиморфизму: он подразумевает решение одинаковых проблем разными способами (например, генерация отчетов для разных систем обработки зарплат), и часто применяется в приложениях на высоком уровне для обработки разных вариантов. Но мир, в этом отношении, не является черно-белым, имеется много оттенков и исключений.
Ссылки
Объекты и полиморфизм |
---|