Получив общее представление о базах данных и освоив первую модель данных ("сущность —связь"), можем приступить к изучению других моделей данных. В этой главе мы бегло рассмотрим иерархическую и сетевую модели в их старых реализациях, относящихся к 70-м - 80-м годам прошлого века, и, подробно, современную иерархическую модель, используемую в СУБД Cache. В следующих главах с разной степенью подробности будут изучаться объектная, объектно-реляционная и другие модели. Подобное изобилие не должно пугать. Во-первых, они всё равно существуют, нравится ли нам это или нет. Во-вторых, мы освоим отображения моделей, что позволит "свернуть" знания, выработать единый взгляд на базы данных, независимо от используемых в них моделей данных.
Иерархии так широко распространены в моделях окружающей нас действительности, что профессионал в области баз данных не может без них обойтись. Если вы не будете работать с иерархическими моделями данных, то уж моделировать иерархии в рамках используемых моделей вам придется обязательно.
Возможно, где-нибудь вы прочли о том, что иерархическая модель была первой реализованной в базах данных моделью. Это так. Обычно за этой фразой следует утверждение о том, что иерархическая модель представляет только исторический интерес, или о том, что иерархические базы данных в настоящее время не существуют. Вот это уже неправда. Давайте разберемся.
Прежде всего, иерархии настолько распространены, что трудно представить игнорирующую их модель сколько-нибудь интересного фрагмента Мира.
Во-вторых, в информатике иерархические структуры данных встречаются на каждом шагу. Достаточно вспомнить структуру каталогов нашего компьютера или любого сайта Интернет.
В этом разделе будут бегло рассмотрены не иерархические и сетевые модели вообще, а те ограниченные их варианты, которые доминировали на рынке баз данных в 60-е и 70-е годы прошлого века. В настоящее время иерархии чаще всего связаны с XML, либо эмулируются в других моделях, например, реляционной, а сетевые модели это скорее всего семантические сети.
Зачем нужно что-то знать об устаревших моделях? По трём причинам. Во-первых, мы по возможности будем вырабатывать единый подход к любым моделям данных и строить отображения между ними. Для этого нужно знать сами модели. Точное представление иерархической, сетевой и реляционной моделей в СУБД Cache позволит нам рассматривать их отображения в главе 10. Во-вторых, для правильного понимания современного состояния дел полезно иметь какое-то представление о прошлом. В-третьих, методы доступа к данным до сих пор во многом основываются на разработках, выполненных в рамках тех старых моделей.
Помня о том, что само понятие модели данных появилось в связи с разработкой реляционной модели, перечислим основные особенности реализаций ранних моделей данных:
Конечно, перечисленные особенности относятся скорее к реализациям, чем к самим моделям. Тем не менее, классические сетевая и иерархическая модели вошли в историю именно с такими недостатками.
Существовало несколько определений сетевой модели данных. В некоторых допускались бинарные связи "многие ко многим". В варианте, предложенном CODASYL, допускаются только связи типов $$1 : n$$ и следовательно $$1:1$$. Это представление и будет рассмотрено.
Сетевая модель строится из элементов данных, типов записей и типов наборов. Как станет понятно в следующих главах, их аналогами в реляционной модели являются атрибут, отношение и связь между отношениями, соответственно.
Графически такая сеть представляется диаграммами, в которых прямоугольники изображают типы записей, а стрелки, имеющие наименования, типы набора. Для связи типа 1 : n между двумя типами записей стрелка должна исходить от типа, называемого владельцем набора, к типу, называемому членом набора (рисунок 3.1).
(рис 3.1) Тип набора
Пожалуйста, не путайте наборы значений, введённых в главе 2, и рассматриваемые здесь типы наборов, объединяющие типы записей.
Заметим, что аналоги типа набора известны и в языках программирования общего назначения. Это записи с повторениями значений элемента данных. Существуют многочленные наборы, у которых на одного владельца набора приходится более одного члена набора (рисунок 3.2), выделен класс сингулярных типов набора (рисунок 3.3), которые используются для объединения записей, не имеющих естественного владельца, либо для включения записей, которые при вводе в базу не имеют естественного владельца, но могут приобрести его впоследствии (при этом, запись перемещается из сингулярного набора в экземпляр набора с новым владельцем записи).
(рис 3.2) Многочленный набор
(рис 3.3) Сингулярный набор
Наборы реализуются программно с помощью указателей, устанавливающих связь между владельцем и членом набора. Для создания связи $$1 : 1$$ в запись-владелец отношения включается дополнительное поле, представляющее указатель на запись-член отношения. Для организации связей $$1 : n$$ и $$n : m$$ вводится дополнительный тип записей, полями которого являются указатели.
В сетевой модели тип записи может быть членом нескольких наборов. В одних он может играть роль владельца набора, а в других — роль члена набора. В этом случае вводится несколько дополнительных групп указателей, а в типе-владельце набора добавляют несколько полей, указывающих на эти группы.
В итоге множество типов записей и типов связей между ними образует сетевую структуру представляемую ориентированным графом. Вершины этого графа —типы записей, его дуги, направленные от владельца к члену набора, — связи между типами записей. Операции над данными в сетевой модели реализуются через действия со списковыми структурами.
Для того чтобы понять, какие проблемы возникают при работе со списковыми структурами рассмотрим два варианта реализации указателей: с указателем следующей записи и с массивом указателей.
Вариант с указателем следующей записи (рисунок 3.4) обеспечивает последовательный доступ к записям-членам набора. По экземпляру записи-владельца набора обеспечивается последовательный просмотр записей-членов наборов.
(рис 3.4) Набор с указателем следующей записи
Если удалить запись, то в предыдущей записи необходимо скорректировать указатель. Это недостаток. Достоинство —экономия памяти.
Вариант с массивом указателей отличается независимостью указателей, но увеличивает их число (рисунок 3.5).
(рис 3.5) Массив указателей
Из-за общности сетевой модели в ней может быть представлена любая ER-диаграмма. К недостаткам сетевой модели можно отнести сложность получаемой на её основе концептуальной схемы и трудность её восприятия пользователем.
Реализация наборов в иерархической модели, как и в сетевой, может осуществляться с помощью указателей. Иерархическая модель отличается от сетевой модели рядом особенностей. Многочленные наборы определяют отношения соподчиненности. Тип —владелец набора называется предком, подчиненный тип —потомком. Особенности иерархической модели:
Пример иерархической модели приведен на рисунках 3.6 и 3.7. Штриховыми линиями выделены виртуальные узлы и связи, которые обеспечивают физическое хранение единственного экземпляра записи, воспринимаемого на логическом уровне одновременно как экземпляр типов "Руководитель" и "Работник".
(рис 3.6) Пример древесного типа
(рис 3.7) Заполнение иерархической базы
Иерархическая база данных может представлять совокупность нескольких деревьев. В частности, некоторые из них могут описывать структуры данных, а другие — сами данные, размещённые в этих структурах. Дополнительные горизонтальные ссылки могут существовать между узлами нескольких уровней, в том числе находящихся в разных деревьях. Такие ссылки позволяют использовать один набор записей в нескольких деревьях, не дублируя его. Широкое распространение иерархической модели было обусловлено, в частности тем, что большинство организационных систем в то время имели иерархическую структуру.
Программы, реализующие операции иерархической модели, существенно проще, чем аналогичные программы для сетевой модели. При выполнении операций манипулирования данными в иерархической модели следует учитывать, что узел потомка не может существовать при удалении предка. Недостатки у иерархической модели те же, что у сетевой, плюс недостатки, связанные с ограниченностью связей.
В незапамятные, по меркам области информационных технологий, времена, в 1967 г. был создан язык MUMPS. Эта аббревиатура полного названия "Massachusetts General Hospital's Utility Multiprogramming System", была неудачна уже по тому, что слово mumps означает болезнь, именуемую в просторечии свинкой. В 1977 г. язык был стандартизирован ANSI/ISO сразу после Фортрана и Кобола и в середине 90-х переименован в язык M. Катастрофически не везло языку с именами. Не согласны — попробуйте найти сведения о нём, задав поисковику ключевое слово "M". Современную версию языка М, называемую вполне приличным именем Cache ObjectScript (сокращённо, COS), поддерживает фирма InterSystems International.
Система управления базами данных Cache (это слово в переводе с французского означает то, что приносит известность или престиж) и правда обладает многими достоинствами. Почему для изложения материала книги, которую вы в настоящий момент, как я надеюсь, читаете, выбрана именно Cache? А потому, что это единственная СУБД, в которой можно не только поработать с тремя моделями данных —иерархической, реляционной и объектной — и изучить их отображения, но и самим создавать другие модели данных, что и будет сделано в конце книги. В этой главе мы освоим основные изобразительные средства языка и, особенно, работу с древесными структурами. В первых разделах будем изучать Cache ObjectScript так, как изучают какой-нибудь QBasic в школе. Это позволит приобрести необходимые начальные навыки и не отвлекаться на мелочи при работе с деревьями.
Для нас важно, что ObjectScript — это персистентный язык. Это означает, что он работает со структурами данных, хранящимися на диске. И эти структуры представляются деревьями, может быть связанными между собой. Их мы будем изучать подробно. Позже вы узнаете, что и программа в ObjectScript—это дерево специального вида.
Не считайте, что приобретённые в процессе скитаний по деревьям знания и навыки, нужны только в Cache. В любой современной СУБД используют XML-модели, имеющие древесную структуру. Так что вы всегда и везде будете на должном уровне.
Однопользовательская версия Cache может быть инсталлирована и использована без ограничений для некоммерческих целей. Это означает, что вы можете даже создавать приложения, но не имеете права их коммерческого использования.
Заходим на сайт фирмы InterSystems (http://www.intersystems.ru) и скачиваем файл инсталляции (путь "Продукты > Cache > Скачать Cache > Скачать однопользовательскую версию Cache"). Все примеры в этой книге демонстрируются на версии установленной из файла Cache-2010.2.0.317-win_x86.exe Объём файла 292 мбайта. Существуют версии под ОС Windows, Unix, Linux, OpenVMS и Mac OS X.
Для инсталляции под Windows запустите файл инсталляции и дождитесь появления страницы с лицензионным соглашением (рисунок 3.8). Внимательно изучите его и подтвердите, что вы принимаете условия соглашения. Нажмите на кнопку "Далее".
(рис 3.8) Страница лицензионного соглашения
Выберите имя инсталляции, а лучше оставьте имя по умолчанию. Еще раз нажмите на кнопку "Далее". Появится путь, в котором устанавливается Cache. Его также можно изменить. Ещё раз нажмите на "Далее". На следующем экране (рисунок 3.9) выберите вариант по умолчанию "Разработка". Затем нажмите на кнопку "Далее".
(рис 3.9) Вид инсталляции
Остаётся выбрать кодировку—8 бит или Unicode. Выбирайте Unicode, затем параметры безопасности (ставьте на минимум). Нам они пока безразличны.
Наконец, после того, как программа выдаст итоговые сведения о варианте инсталляции, нажимаем на кнопку "Установить" и ждем завершения инсталляции.
Если Cache была правильно установлена, то внизу справа на панели задач появится изображение синего кубика. Называют его Cache-куб. Если щёлкнуть правой кнопкой по Cache-кубу, появится всплывающее меню Cache (рисунок 3.10).
(рис 3.10) Меню Cache
Первое время нас будет интересовать единственный пункт меню — "Терминал" (рисунок 3.10). Позже мы будем использовать студию и часть функций портала управления системой. Итак, выбираем пункт меню "Терминал". Окно терминала выглядит примерно так, как показано на рисунке 3.11.
(рис 3.11) Начало работы с терминалом
Подсказка USER> приглашает начать работу в пространстве имён (или в базе) USER. Всегда существуют две обязательных области имен %SYS и %CACHELIB и сколько угодно необязательных, в том числе USER. Пространство имён — это множество уникальных имён, обычно как-то связанных по смыслу. Одно имя может повторяться только в разных пространствах имён.
Область имён всегда можно сменить командой ZN:
USER>ZN "SAMPLES" SAMPLES>
Кстати, имена команд не чувствительны к регистру.
Займемся основными изобразительными средствами языка.
Вспомним, что формы Бэкуса-Наура применяются для описания синтаксиса языков программирования. Терминальным называется символ, который может быть записан в порождаемую строку текста без изменений. Нетерминальные символы разворачиваются на основе приведенных описаний.
Нетерминальные символы будем записывать в виде одного слова или нескольких слов, соединённых знаком подчёркивания, например, логиче-ское_выражение. Для каждого нетерминального символа должно существовать минимум одно определяющее его правило.
Такое правило имеет формат:
$$нетерминальный\_символ::=определение$$
Знак "::=" означает "определен как ". Используются ещё разделитель альтернатив в виде вертикальной черты "|", признак необязательного выбора [], признак обязательного выбора {} и признак повторяемости в виде многоточия ". . . ".
Символ $$\sqcup$$ в записи синтаксиса обозначает пробел. Символ $$\hookleftarrow$$ — конец строки — соответствует нажатию клавиши Enter.
Заметим, что из-за сложности разбора полного синтаксиса для начинающих в этой главе будут приводиться только неполные описания, достаточные для усвоения основ. За деталями следует обращаться к материалам фирмы InterSystems, например, выбрав позицию "Документация" (рисунок 3.10).
В одной строке можно размещать несколько команд. Определение командной строки для работы в терминале:
$$командная\_строка ::= команда[\sqcupкоманда...] \hookleftarrow$$
Командная строка состоит из одной или нескольких команд. Каждая команда состоит из командного слова, следующего за ним одного пробела-разделителя и аргументов, разделенных запятыми. Команды в одной строке разделяются не менее чем одним пробелом. За командой без аргументов должно следовать не менее двух пробелов. Количество команд в строке ограничивается лишь заданием максимальной длины строки.
Иногда можно задать несколько аргументов команды, разделенных запятыми. Если хотите, можно после запятых помещать один или несколько пробелов. Можно пробелов после запятых не делать вообще.
В число аргументов команд ввода (READ) и вывода (WRITE) можно включать символы форматирования:
Команда WRITE обеспечивает вывод информации в терминале. Допускается использование как полного имени команды WRITE, так и сокращенного до одной буквы W.
Синтаксис команды WRITE:
$$W[RITE][:постусловие]\sqcup[форматирование | аргумент\_WRITE], \dots$$,
где форматирование—один из символов !, ?, \#;
за символом ? следует число;
аргумент\ WRITE — переменная, константа или выражение.
Постусловия —это условия, при выполнении которых команда будет исполнена, иначе команда пропускается. Необходимость понятия аргумент станет понятна при изучении косвенности (раздел 3.2.4).
Заметим, что все имена команд не чувствительны к регистру.
В конструкции "?число", аргумент не должен превышать 32767. Да и зачем нам такое число пробелов? Числа меньше нуля или 0 не сдвигают маркер. Дробная часть числа просто отбрасывается и аргументом считается оставшаяся целая часть. Иначе говоря, минимальный шаг — это знакоместо.
WRITE без аргументов выводит на терминал имена и значения всех существующих на данный момент в памяти переменных, называемых локала-ми. Выполните командную строку W 0, ?20, "a", ?40, "b", !, 0 , попытавшись предсказать результат до выполнения команды на машине.
Применение перехода к новой странице — знак диез (#) — в общем случае имеет смысл, только если печатается твердая копия (в COS можно перенаправлять вывод на любое устройство). Команда W # очищает экран.
Присваивание выполняется командой SET. Её формат:
$$S[ET][:постусловие]\sqcup [присваивание][,присваивание \dots]$$
Присваивания являются аргументами команды.
Присвоим переменной x значение 1 командой $$SET\ x=1\rfloor$$ . Проверить, была ли создана переменная x, можно, набрав команду $$WRITE\ x\rfloor$$. Результатом выполнения этой команды является вывод на экран значения переменной x. Действительно оно равно 1 (листинг 3.1).
USER>SET x=1 USER>WRITE x 1 1 USER>
Вертикальную черту слева вы не увидите в терминале. В книге она обозначает что представлена картина, полученная в терминале.
Вы, конечно, заметили, что редактор терминала — строчный. Например, набрав WRITTE x, мы можем клавишей — подвести знак маркера к лишней букве "T" и удалить её клавишей Delete.
Выполнявшиеся ранее команды запоминаются. Последняя выполненная команда вызывается клавишей $$\uparrow$$. Каждое нажатие на неё вызывает предыдущую команду, пока все сохранённые записи не исчерпаются. Клавиша $$\downarrow$$ используется для обратного прохода по этим же командам.
Вспомним, что командные слова не чувствительны к регистру, так что в дальнейшем позволим себе вольности и будем писать их и заглавными и строчными буквами.
Командные слова сокращаются, как правило, до одной буквы. Так, вместо строки SET x=1 можно написать S x=1. Если же нужно создать несколько переменных, то это можно сделать следующим образом:
|USER>S z=1, Ы="", h=1.2
Для проверки значений всех существующих на текущий момент переменных можно распечатать их значения при помощи команды WRITE без параметров.
Замечание. Cache одинаково хорошо работает с обеими половинами таблицы ASCII. Так что кириллица используется без ограничений.
В дальнейшем изложении знак $$\hookleftarrow$$ проставляться не будет, так как его применение достаточно очевидно — обозначение конца командной строки и конца ввода.
Синтаксис команды READ ввода данных с терминала:
$$R[EAD][:постусловие]\sqcup[[подсказка,]аргумент] $$
В качестве аргументов используются имена переменных.
Форматирование включается в подсказку.
При исполнении команды система ждёт ввода данных Завершается ввод клавишей Enter. Система позволяет ограничить время ввода. Если набрать $$ R x:3 $$и ничего не вводить, то через 3 секунды ожидание ввода прекратится (листинг 3.2).
USER>R x:3 USER>
Точно также можно ограничить количество вводимых символов. Так, команда R x#4 позволяет ввести не более четырёх символов. Можно сочетать оба ограничения # и :. Например, R x#4:8.
Команда READ позволяет вводить значения с любого внешнего устройства, но мы этот вариант рассматривать не будем.
Примеры команды приведены в листинге 3.3.
USER>R ?10,"Введите ваше имя: ",nn Введите ваше имя: Николай USER>R ?5,"Введите ваше имя: ",nn,!,?5,"Фамилию: ",sn Введите ваше имя: Николай Фамилию: Бессарабов
Каждое введенное значение завершается нажатием на клавишу Enter. Проверьте командой WRITE введенные значения фамилии и имени.
Стирать созданные переменные можно командой KILL, сокращённо K. При этом KILL без параметров удаляет все локальные переменные. В варианте "KILL список_имён" удаляются все указанные переменные, а "KILL (список_имён)" удаляет все переменные кроме указанных (листинг 3.4).
USER>K s a=1,b=2,c=3,d=4,bc=7 7 W a=1 b=2 bc=77 c=3 d=4 USER>K a,b W bc=77 c=3 d=4 USER>K (c,d) W c=3 d=4 USER>
В первой командной строке командой K мы удалили все имевшиеся локальные переменные и создали локалы a, b, c, d, bc. Обратите внимание, что здесь после K стоит ровно два пробела. Во второй командной строке "K a, b W" удаляются переменные a и b и выводятся все оставшиеся локалы. В третьей строке "K (c, d) W" удаляются все переменные, кроме c и d, то есть удаляется bc.
В командах SET и WRITE можно вычислять сложные выражения. При этом следует помнить, что в Cache для ускорения разбора принято расшифровывать выражения слева направо, не учитывая старшинство операций. Поэтому 2 + 3 * 8 равно 40, а не 26, как учили в школе (листинг 3.5). Пользуйтесь скобками для задания порядка действий.
USER>W 2+3*8 40 USER>W "12qwerty"+4 16 USER>W "qwerty12"+4 4 USER>
Все переменные не типизированы. По сути, хранится единственный тип данных — строка переменной длины. Последовательность символов в зависимости от контекста расшифровывается слева направо, пока это возможно. Поэтому "12qwerty" в арифметическом выражении равно 12, а "qwerty12" нулю.
Выражения могут быть трех типов:
| Оператор | Выполняемое действие |
| \ | Целочисленное деление |
| ** | Степень |
| # | Остаток от деления по модулю |
| _ | Конкатенация (соединение строк) |
| ' | Логическое отрицание (NOT) |
| = | Присваивание или сравнение на равенство |
| ‘= | Сравнение на неравенство |
| [ | Содержит |
| ] | Следует за |
| ]] | Сортируется за |
| , | Логическое AND ( это "короткое" AND) |
| ||, ! | Логическое OR (|| это "короткое" OR) |
| @ | Косвенность |
| ? | Сравнение с шаблоном |
Допускаются комбинации '> не больше чем (меньше или равно) и '< не меньше чем.
Основу логических выражений образуют операторы сравнения (=, >, <, '=, [, ], ?). Заметим, что обозначение для неравенства <> не употребляется. А вот условия <= и >= можно использовать наряду с '< и '>.
Например, R x IF x <=2 W x,"<=2"
Обратите внимание, что в качестве одиночной кавычки выбирается та, которая расположена на клавише с буквой "э".
Операторы сравнения и логические константы связываются логическими операторами AND (обозначается или ), OR (обозначается | или ||), NOT (обозначается ').
Предскажите и проверьте результат выполнения трёх командных строк с постусловием:
W:"1ab" "Условие истинности" W:-1 "Условие истинности" W:0 "Условие истинности"
Ответ: Первое постусловие "^^'представляет константу, интерпретируемую как логическое значение 1. Второе условие в виде константы —1 также интерпретируется как "не 0", то есть 1. И только третье соответствует ложному значению.
Краткие варианты логического оператора "И" и || оператора "ИЛИ" отличаются от обычных и | тем, что выполнение цепочки связанных условий прекращается, как только результат определится. Для "И" достаточно получить первый ложный результат, а для "ИЛИ" первый истинный. Условия в IF, записанные через запятую, также выполняются по краткому варианту. Поэтому для повышения скорости, необходимо первыми помещать те условия, у которых больше шансов получить ложное значение для "И" и истинное значение для "ИЛИ".
Логические и арифметические выражения могут сочетаться. Логическое выражение может использоваться для выбора арифметического значения, например, $$(11*(x>7))+(27*(x<=7))$$ .
Для тренировки объясните работу следующих командных строк:
$$ S\ x=3\ W\ 11*(x>7)+27*(x<=7) $$
и
$$ S\ x=8\ W\ 11*(x>7)+27*(x<=7) $$
Мы уже обратили внимание на то, что Cache очень лояльна к именам из второй половины ASCII таблицы. В других СУБД с ними могут возникать большие проблемы. Скажем, такие имена допускаются при создании объектов, которые затем работают с ошибками. Имена переменных должны начинаться либо с буквы, либо со знака процента (%), за ними ногут следовать буквы или цифры. Системные переменные могут начинаться с символа $.
Длина имени не ограничена, но в идентификации имён участвуют только первые 31 символов. Пользовательские имена чувствительны к регистру, а предопределённые имена команд нет.
В COS нет проверки на совпадение пользовательских имён с зарезервированными именами. Можно создать переменную с именем set, например, SET set=11, но так поступать не рекомендуется. Вы можете запутать и себя и других разработчиков.
Язык COS способен создавать переменные, значения которых сохраняются на диске. В противовес временным переменным, именуемым лока-лами, хранимые переменные называются глобалами. Их имена обязательно начинаются со знака "^". Как остроумно сказано в одной книге, это галочка, которая в простонародье именуется циркумплекс. Действия над глобалами осуществляются так же, как и с обычными переменными — локалами. Но WRITE без параметра их не обнаруживает.
Даже если вы ещё не создавали глобалов, то в выбранном вами пространстве имён, например USER, они уже существуют. Для проверки войдем в портал управления системой и в разделе "Управление данными (System Explorer)" выберем "Глобалы", а затем пространство USER (рисунок 3.12).
(рис 3.12) Глобалы, созданные при инсталляции
Командой S ^а=33 в терминале создаём глобал и проверяем его наличие нажав на "Просмотр глобалов"(рисунок 3.13).
(рис 3.13) Глобал ^a создан
Выбираем "Просмотр" или "Редактировать". Теперь можно просмотреть глобал и удалить его, а в глобале с древесной структурой удалить его весь или же отдельные узлы (рисунок 3.14).
(рис 3.14) Редактирование глобала
Глобалы пространства имён XX хранятся в папке с именем XX в файле с одним и тем же именем Cache.dat. На моей машине созданный глобал^а находится в файле Cache.dat в папке C:\InterSystems\Cache\mgr\user. У вас вместо имени Cache может использоваться TryCache.
Заметим, что процессом сохранения созданных или изменённых глоба-лов пользователь управлять не может.
Предполагалось, что, выполняя предыдущие примеры, вы не делали ошибок. Вообще говорят, что люди делятся на умных, которые учатся на чужих ошибках и других, которые учатся на своих. Так вы этому не верьте. По крайней мере, в программировании это не так. Хорошо бы, конечно, никогда не делать ошибок. Но в реальности остаётся стараться не делать их слишком много, особо избегая непоправимых ошибок, и, самое главное, быстро исправлять сделанные ошибки. Научиться этому можно, только на своих ошибках.
Итак, вы решили определить переменную с неправильным именем 2x задав команду S 2x=1. Получаем сообщение об ошибке (листинг 3.6, первая командная строка). Ошибка (<SYNTAX>) означает синтаксически неправильно сформированную строку языка Cache ObjectScript.
Сделаем ещё одну ошибку, которая позволит выяснить, как исполняется командная строка (листинг 3.6, вторая командная строка). Ошибка <UNDEFINED> сигнализирует о том, что некоторая переменная не имеет значения. Имя переменной пишется после знака *.
USER>S 2x=1 S 2x=1 ^ <SYNTAX> USER>K S x=1,y=2,z=a,v=3 K S x=1,y=2,z=a,v=3 л ^ <UNDEFINED> *a USER>W x=1 У=2 USER>
Вторая ошибка произошла из-за того, что переменной z мы пытались присвоить значение неопределенной переменной а. Результат выполненной позднее в третье строке команды W показывает, что командная строка читается слева направо и те её части, которые могут быть выполнены до появления ошибки (S x=1,y=2) выполняются. Появление ошибки прекращает исполнение командной строки и присваивание S v=3 не производится, хотя эта часть команды ошибки не содержит.
Разветвления в COS могут организовываться тремя способами:
IF, отдельная команда ELSE, системная переменная $TEST, хранящая логическое значение истинности последней проверки условия);IF-ELSEIF-ELSE, переменная $TEST при работе с условиями не используется).Главное различие между вариантами IF в том, что классическая команда основана на разбиении программы на блоки, а старая команда языка М этого не предполагает, хотя и допускает.
Блоком называется последовательность команд COS, заключенная в фигурные скобки. Блок может быть помещен везде, где может помещаться единственная команда. Блоки могут быть вложены.
Любая команда может быть снабжена необязательным постусловием, записываемым через двоеточие:
$$имя\_команды[:<логическое выражение>]$$
Команда с постусловием выполняется только если логическое выражение принимает значение ИСТИННО. Вспоминаем, что в COS принята следующая трактовка числовых значений истинности: 0 это ложно, любое другое значение — истинно. В качестве примера вводите разные значения, исполняя командную строку
R z W:z=1 !,"z=1'"
В команде передачи управления GOTO постусловия могут следовать как за командным словом GOTO, так и за любым ее аргументом:
$$G[OTO][:логическое_выражение]?[аргумент][,аргумент\dots]$$,
где
$$аргумент ::= метка[:логическое\_выражение] | @простейшее\_выражение$$
Последний вариант $$@простейшее\_выражение$$ это косвенность, которой мы займёмся позже.
Команда
$$\sqcup G:x>=1 METKA1:y=1, METKA2:y>1,METKA3$$ работает, если выполнено условие $$x > 1$$, при этом переход производится к первой метке если $$y = 1$$, ко второй если $$y > 1$$ и к третьей в остальных случаях $$(y < 1)$$. Необходимость простановки пробела перед G объясняется тем, что команду имеет смысл использовать не в командной строке, а в программе. В строке программы команды записываются со второй позиции, а сами программы рассмотрим чуть позже.
Заметим, что переход из GOTO в другую программу или другую строку той же программы выполняется без возврата в следующую за вызвавшей строку. Если требуется возврат управления, используют команду D[O], в которой за командным словом также может помещаться постусловие.
Замечание. В COS метка —это любое имя. В инструментальном средстве "Студия", которую мы рассмотрим ниже, метка записывается с первой позиции строки.
Формат команды IF:
$$\sqcup I[F] <аргумент>[,<аргумент>...] <все\_команды\_до\_-конца\_строки>$$,
где
$$<аргумент>::=<логическое\_выражение>|@<проcтейшее_выражение>$$
Каждый аргумент —это условие, возвращающее логическое значение. Системная переменная $TEST хранит логическое значение последней проверки. Набор аргументов команды IF образует условие IF (пример 3.7)
|USER>S x=1 I x=1 W "x=",x, " $TEST="_$TEST
Если в IF имеется несколько аргументов, то они образуют сложное логическое условие в котором аргументы соединены логическим оператором И (AND).
Команда ELSE сделана независимой от IF. Ее формат:
$$E[LSE]\sqcup\sqcup<все\_команды\_до\_конца\_строки>$$
Часть строки, идущая вслед за ELSE выполняется, если $TEST=0. В противном случае управление передается следующей строке. Обратите внимание, что после ELSE записывается два пробела (пример 3.8)
USER>S x=1 I x=2 W "x=", x USER>W "$TEST="_$TEST USER>E W x
Существует еще безаргументный IF, записываемый в виде:
$$I[F]\sqcup\sqcup<все\_команды\_до\_конца\_строки>$$
Понятно, что он использует последнее значение $TEST, установленное другими командами. При сложных проверках, осуществляемых в ветвящихся и циклических программах, использование такого синтаксиса может сильно затруднить отладку.
Заметим, что постусловия, в отличие от команды IF, не используют переменную $TEST и потому они не могут повлиять на команду ELSE.
Все ветви разветвления передаются единственной командой:
$$\verb*|I[F] <аргумент>[,<аргумент>,...]{<блок>}|$$
$$\verb*|ELSEIF <аргумент>[,<аргумент>,...]{<блок>}|$$
$$\verb*|ELSE {<блок>}|$$
Например:
$$\verb*| R x|$$
$$\verb*| I x=2 \{W !,"x=2"}|$$
$$\verb*| ELSEIF x=3 \{W !,"x=3"}|$$
$$\verb*| ELSE \{W !,"???"}|$$
$$\verb*| Q|$$
Вы, конечно, обратили внимание, на необычную запись последнего примера и приведённой выше команды GOTO . Дело в том, что многострочная запись команды в терминале не работает и реализовать ее можно только в программе, создаваемой в инструментальном средстве Студия. А там действует правило: с первой позиции в строке записываются только метки программ, а команды занимают позиции со второй и далее. Упомянутая команда GOTO также предназначена для использования в программе.
Запустим из меню куба Cache ещё один инструмент — Студию (рисунок 3.15).
(рис 3.15) Студия
При выборе в подменю позиции "Создать" будет предложен выбор варианта. Для создания программы будем останавливаться на выборе по умолчанию "Cache ObjectScript программа". В главе 10 в студии будут создаваться классы (вариант "Класс Cache"). Многочисленные остальные возможности в этой книге не используются.
Наберем текст IF в точности так, как указано в последнем примере. Попробуйте набрать какую-нибудь команду с начала строки. На поле слева появится красная стрелка - признак ошибки в строке. Текст команды будет подчёркнут красным.
Есть два варианта компилирования программы. В первом выберите в меню последовательность "Файл>Сохранить как", затем укажите имя программы, например P1. Расширение имени пока роли не играет. Оставьте предлагаемое расширение .mac. Обратите внимание на то, что имя программы чувствительно к регистру, так что P1 и p1 это разные имена.
Во втором варианте выберите в головном меню позицию "Собрать", а затем "Компилировать" (или просто наберите комбинацию клавиш Ctrl+F7). В любом варианте выполняется компиляция.
Поскольку программа создана успешно и сохранена в виде глобала AP1.mac, ее можно вызвать из терминала с помощью команды D[O] —ВЫПОЛНИТЬ. Расширение имени указывать не обязательно. Проверьте в терминале её работу (рисунок 3.16)
(рис 3.16) Первая программа
Переменная $TEST в этой форме разветвления не задействована.
В ObjectScript, как и в большинстве языков программирования, имеются три основных типа циклов:
"FOR";"WHILE";"DO ...WHILE".Структуры команды FOR:
$$\verb*|F[OR] счётчик\_цикла=параметр программная\_строка|$$
$$\verb*|F[OR] счётчик\_цикла=параметр {блок}|$$
$$F[OR] блок$$
В однострочном варианте тело цикла образуют все команды, помещённые после команды FOR до конца строки. В простейшем случае параметр имеет одну из форм:
$$список\_выражений\_разделённых\_запятой;$$
$$начальное\_значение:инкремент;$$
$$начальное\_значение:инкремент:конечное\_значение$$
Значения счётчика цикла могут и возрастать и убывать и принимать числовые и строчные значения вперемешку.
В сокращённых версиях цикла отсутствует проверка конца. Поэтому: необходимо в теле цикла организовать выход из цикла, например,
F i=1:1 W i, ! s i=i+1 Q:i>5
Примеры:
F i=1:1:5 W i, !В Студии можно использовать циклы с блоками (листинг 3.9)
FOR {
READ !,"Number:", num
QUIT:num=""
W !,num
}
Заметьте, здесь использована простейшая безаргументная форма цикла.
Можно использовать сколько угодно параметров FOR.
Пример: F i=1:1:4, "X", 23:2:30 w !, i
При зацикливании программы наберите сочетание клавиш Ctrl/C. А теперь сами попытайтесь предсказать, как будет работать цикл: F i=1:(i<3):4 W !, i
WHILE — цикл с предусловием, DO —WHILE — цикл с постусловием. Их форматы:
$$\verb*|WHILE условие блок|$$
и
$$\verb*|DO блок WHILE условие|$$
Условие завершения в цикле WHILE выполняется до выполнения блока, а в цикле DO-WHILE после. Если значение истинно (то есть отлично от 0), то осуществляется новая итерация цикла WHILE, в противном же случае цикл завершается.
Пример цикла WHILE приведен в листинге 3.10
SET i=0
WHILE (i<10) {
SET i=i+1 WRITE i,!
}
QUIT
Результатом выполнения данной программы будет вывод на экран чисел от 1 до 10.
Выход из тела цикла возможен с помощью команды передачи управления по метке (GOTO).
В зависимости от промежуточных результатов полученных программой может меняться ход вычислений. Разветвления и циклы меняют направление прохода по алгоритму и число повторений циклов. Может потребоваться значительно больше: изменить имена, параметры или индексы, сформировав их в программе. Эти проблемы решаются использованием косвенности. Максимально гибкими оказываются программы, которые могут исполнить на ходу сформированные фрагменты текста. Для этого используется команда со странным названием XECUTE, образованным от слова execute (исполнить).
Заметим, что программы, изменяющие себя самих без всяких ограничений, конечно же, интересны, но вряд ли удобны для работы с бизнесом. Представьте, что, написав программу сегодня, назавтра вы уже не знаете, с какой именно программой работаете. Полезными могут оказаться программы, у которых меняются отдельные разделы.
Косвенность позволяет сформировать меняющиеся имена переменных и программ, аргументы, индексы и элементы шаблонов. Различают четыре варианта:
Последним вариантом —косвенным шаблоном —мы заниматься не будем из-за того, что шаблоны в ObjectScript плохо приспособлены к работе с кириллицей.
Рассмотрим косвенное имя. Косвенность в имени обозначается символом @, проставляемым перед любым простым выражением. Простое выражение — это любое выражение языка, не содержащее бинарных операторов. Можно использовать и выражение с бинарными операторами, заключенное в скобки.
Начнем с простого примера (листинг 3.11, первая командная строка).
(1) USER>S a="b",b=77, w a,!, @a
b
77
(2) |USER>S a1=1, a2=2, a3=3 F i=1:1:3 W @("a"_i)
123
(3) |USER>S a1=1,a2=2,a3=3, ab="ab" F i=1:1:3,"b" W @("a"_i)
123ab
Здесь мы задали переменной a значение "b", переменной b присвоили значение 77 и распечатали командой WRITE сначала значение а, затем @a. Поскольку использована косвенность, печаталось значение той переменной, имя которой записано в а, то есть 77.
Следующие два примера (листинг 3.11, вторая и третья командные строки) показывают формирование имени переменной в цикле с помощью косвенности. Имена переменных создаются в цикле с помощью конкатенации. Это позволяет в цикле читать набор переменных a1, a2, a3 не образующих массива. Скобки в операторе W обязательны. Иначе косвенность распространялась бы только на переменную a, тогда как необходимо использовать выражение "a"_1.
Косвенность вкладывается в косвенность на любую глубину, например, S a="b", b="c", c=9 w a, !, @a, !, @@a
Предскажите, а затем проверьте результат выполнения следующей строки:
S a1=1, a2=2, a3=3 F i=1:1:3 W @"a"_i
Для того, чтобы завершить тему косвенного имени рассмотрим пару примеров, которые рекомендуется выполнить после изучения программ в ObjectScript (раздел 3.2.8). Пусть имеется любая программа PRG.INT с меткой MET1. Косвенность применима к именам программ и к меткам:
USER>S rout="prg" D A@rout USER>S a="MET1", b="PRG" D @aAPRG, MET1A@b, @aA@b
В последнем примере три раза подряд вызывается PRG с метки MET1.
Вызов программы с косвенным именем может снабжаться постусловием: |USER>D A@rout:x=1
Порядок действий при использовании косвенности следующий: сначала раскрывается косвенность, затем выясняется существование программы и только потом проверяется условие. Предположим, что вызываемая программа не существует. Если бы сначала проверялось условие, то при его ложности, так и не удалось бы установить, что программа не существует.
Косвенный аргумент позволяет программе задать как один аргумент, так и список аргументов (листинг 3.12, первые четыре командных строки).
USER>S b=77, a="b=2"
USER>S @a W b 2
USER>K b,c S x="b=2,c=3",@x W b," ",c
2 3
USER>S b=2,x="b=2" I @x W x
b=2
USER>S b=2,x="b=2" W:@x x
S b=2,x="b=2" W:@x x
^
<SYNTAX>
В первой командной строке переменной b присвоили начальное значение 77. Значением a во второй строке делаем текст "b=2". Косвенный аргумент в команде второй строки "S @a" раскрывает эту команду как "S b=2", и b присваивается значение 2.
А вот попытка переписать эту строку с постусловием во WRITE не удастся (листинг 3.12, последняя командная строка). Дело в том, что условие, помещаемое после двоеточия это не имя, и не аргумент. Косвенность здесь не применима.
Аргумент цикла FOR не может быть косвенным, то есть нельзя писать, например,
S x="i=1:1:3" F @x W "Z",
но счетчик цикла FOR может использовать косвенность:
S u="i" F @u=1:1:3 W @u
В цикле WHILE ситуация аналогична, то есть единственный вариант использования косвенности это что-то вроде: WHILE @u<3 {S i=i+1 W i}
В сложных случаях форму косвенности определить не легко, хотя общее правило просто и понятно: имеется та форма косвенности, после раскрытия которой получается синтаксически правильное выражение.
Дерево в COS представляется многомерными разреженными масссива-ми. Программа для работы с деревьями должна обеспечить доступ к узлам на любом уровне. Индексная косвенность как раз позволяет формировать индексы во время работы программы. Запись имени узла с использованием индексной косвенности:
$$@имя\_узла@(список\_индексов)$$
Как видите, особенность синтаксиса индексной косвенности в том, что знак @ повторяется дважды.
Создадим локал, имеющий древесную структуру (листинг 3.13) и используя индексную косвенность сформируем команду W, извлекающую значение узла a(1,1,1) .
USER>S a(1)=1,a(2)=2,a(1,1)=11 USER>S a(1,1,1)=111,a(1,1,2)=112 USER>S x="a(1)" w "a(1,1,1)="_@x@(1,1) a(1,1,1)=111
Обратите внимание на то, что индексная косвенность может использоваться только в приведенной выше форме. Иначе говоря, нельзя опускать один из знаков @ и потому, например, команда "S a(1,1)@(2)=77" вызовет сообщение об ошибке.
Команда XECUTE (сокращённо X) позволяет исполнить однострочную подпрограмму, хранимую как значение некоторой переменной:
S z="W 22,", v="!,1+7" X z_v
В результате будет исполнена сформированная командная строка "W22,!,1+7 ".
Заметим, что исполнение команды может передать управление куда угодно, но адрес возврата из XECUTE остаётся прежний —на командную строку, следующую за XECUTE. Для проверки создадим подпрограмму с именем AA следующего содержания:
W "В подпрограмме " Q
Исполним команду XECUTE, вызывающую передачу управления:
USER>X "G AA W 77" W "После подпрограммы",! Q В подпрограмме После подпрограммы
Команда WRITE 77 не была исполнена, что и следовало ожидать.
Мы уже встречались со строковыми константами, представляющими последовательность символов, заключенных в двойные кавычки. Набор используемых символов зависит от сделанного при установке Cache выбора кодировки (Unicode или UTF8). Длина строки не превышает 32 К символов.
Поскольку двойная кавычка ограничивает строковую константу возникает вопрос, что делать, если символ " встречается внутри текста? В этом случае её просто удваивают:
USER>W "a""b" a"b
Длина пустой строки —ноль. В команде READ вводимой переменной присваивается пустая строка каждый раз, когда ввод состоит из единственного нажатия клавиши Enter.
В стандартном ASCII-терминале длина строки 80 символов. При выводе длинная строка разрезается. При вводе необходимо продолжать набор символов, не обращая внимание на самопроизвольный переход маркера на следующую строку (рисунок 3.17).
(рис 3.17) Очень длинная строка
При выборе UTF8 допускается использование символов кириллицы и псевдографики, занимающих вторую половину кодовой таблицы ASCI ОТ 128 до 255).
Над строками определены пять бинарных операций:
Конкатенация соединяет оба операнда в одну строку, записывая операнды последовательно слева направо (листинг 3.14).
USER>W "conca"_"te"_"nation" concatenation
Рассмотрим операторы строкового сравнения =, [ , ] и ]] . Оператор = проверяет строки на идентичность.
Оператор "содержит" обозначается символом "[ " и возвращает логическое значение. Запись s[t означает проверку того, что строка t входит в строку s хотя бы один раз (листинг 3.15):
USER>S x= "abcd"["bc" W x 1 USER>S x= "abcd"["aa" W x 0
Операторы следования и сортировки могут показаться мелочно сложными, не интересными, а может быть и не нужными. Тем не менее, рассмотрите их внимательно и во всех деталях, чтобы понять как Cache упорядочивает узлы дерева одного уровня.
Оператор "следует за" обозначается как закрывающая прямая скобка "] ". Строка s следует за t, то есть выражение t]s истинно, если выполняется одно из трех условий:
строка t непустая, а s пустая, то есть пустая строка следует за любой непустой (листинг 3.16):
USER>S x="abc"]"" W x 1
строки s и t непустые и код ASCII первого символа t превышает код ASCII первого символа строки s (листинг 3.17):
USER>S x="cd"]"axz" W x 1 USER>S x="cd"]"exz" W x 0
начала строк совпадают, но для оставшихся частей выполняется одно из двух предыдущих условий (листинг 3.18):
USER>S x="aacd"]"aabxz" W x 1 USER>S x="aacd"]"aa" W x 1
Оператор "сортируется за", обозначается ]] . В предыдущем операторе "следует за" оказывается, что вопреки здравому смыслу, но в соответствии с правилами лексикографического упорядочения число 10 предшествует 9 потому, что код первого символа "1" меньше кода "9". Так командная строка S x="10"]"9"W x отрицает, что 10 следует за 9. Оператор ]] устраняет этот недостаток. Проверим:
USER>S x="10"]]"9" W x 1
В самом деле, число 10 больше 9, то есть следует за 9.
Поскольку все типы данных, за исключением больших, записываются строками, необходимо интерпретировать строку в соответствие с ее типом. Важно помнить правило числовой интерпретации строки: "Строка в арифметическом выражении читается слева направо и понимается как число до тех пор, пока встречаются символы цифр (0,. . . ,9), буквы "E" или "e" между двумя цифрами, понимаемые как разделитель мантиссы и порядка в экспоненциальной форме записи числа". На первом символе не входящем в указанный набор числовая интерпретация прекращается. Если строку не удается интерпретировать как число, ей присваивается значение ноль (листинг 3.19).
USER>S x="5 apples"+"7 books" W x 12 USER>S x=+"abc" W x 0 USER>S x=-".7e2cccw" W x -70 USER>S x=".7e2cccw" W x .7e2cccw
Работая со строками, нужно уметь определить длину строки (это делает функция $LENGTH), выделить фрагмент строки (функция $EXTRACT), проверить вхождение подстроки в строку (функция $FIND или оператор "[" ). Может потребоваться замена одних символов другими (функция $TRANSLATE).
Функция $L[ENGTH] возвращает длину строки символов. При работе с простыми строками без разделителей используется одноаргументая форма функции (листинг 3.20):
USER>W $L("12345678")
8
USER>W $L("")
0
Функция $E[XTRACT] выделяет из указанной строки подстроку. Второй аргумент указывает начальную позицию, с которой вырезается подстрока, а третий аргумент — конечную позицию (листинг 3.21):
USER>W $E("12345678",3,5)
345
USER>W $E("Those evening bell",7,13)
evening
Второй и третий аргументы, если они имеются, интерпретируются как целые числа.
USER>W $E("Those evening bell",7.9,12.9)
evenin
Если значение второго аргумента, определяющее первую позицию меньше нуля, то выделение начинается с первой позиции строки:
Если третий аргумент превышает длину исходной строки, строка выделяется до конца. Этим пользуются для задания условия "до конца строки". Достаточно в третьем аргументе указать число, превышающее максимально допустимую длину строки.
USER>W $E("Those evening bell",15,9999)}
bell
Если позиция начала подстроки больше позиции ее конца, скажем $E("12345678",10,5), то функция возвращает пустую строку. Пустая строка выделяется и тогда, когда начальная позиция подстроки больше длины исходной строки.
Функция $F[IND] определяет, встречается ли строка заданная ее вторым аргументом в строке заданной первым аргументом. Если подстрока не найдена, то возвращается ноль:
USER>W $F("12345678","x")
0
Если подстрока найдена, возвращается номер позиции следующей за концом найденной подстроки:
USER>W $F("12345678","45")
6
Полезно понять, зачем выдаётся именно эта позиция.
В трехаргументной форме функции $FIND третий аргумент задает номер символа исходной строки, с которого начинается поиск. Поскольку время поиска, пропорционально числу проанализированных символов, задание третьего аргумента может существенно уменьшить время поиска:
USER>W $F("12345678","8",7)
9
Если значение третьего аргумента превышает общую длину исходной строки и второй аргумент не пустая строка, то возвращается ноль.
В соответствии с классической теорией множеств в Cache ObjectScript пустая строка содержится в любой строке, в том числе и в пустой:
USER>W $F("123","")
1
USER>W $F("","")
1
Для поиска всех вхождений подстроки в строку необходимо использовать циклы (листинг 3.22).
USER>S p=1 F {S p=$F("1111", "1",p) Q:p=0 W !,p}
2
3
4
5
Функция $TR[ANSLATE] используется с двумя аргументами
$$$TR[ANSLATE](выражение1, выражение2)$$
или тремя
$$$TR[ANSLATE]( выражение!, выражение2, выражение3)$$
В двухаргументной форме из строки, определённой первым выражением удалятся все символы, имеющиеся в строке, определённой вторым выражением. Например:
USER>W $TR("стpокa","оa")
"стрк"
В трёхаргументной форме строки s2 и s3, определённые вторым и третьим выражениями, определяют таблицу трансляции. Символу в n-ой позиции строки s2 соответствует символ в n-ой позиции строки s3. Если длина s2 больше длины s3, то символы s2, у которых нет соответствия, будут удалены:
USER>W $TR(QUOT;3TO e-mail", "maile-", "мыло") Это мыло
Элементы строк с разделителями называют полями.
Символ-разделитель нельзя употреблять в самих полях. Одной строкой можно записать, например, список фамилий работников отдела "Ива-нов^Петров^Сидоров". В качестве разделителя можно использовать любой печатаемый символ, в том числе и пробел. Естественно, в этом случае поле не должно содержать пробел. Поля также могут быть строками с разделителями, но разделитель внутри поля должен быть другим. Можно и эти поля представить в виде переменных с разделителями и т.д.
Похоже, что многоуровневое вложение полей не очень удобно. Тем не менее, строки с разделителями могут существенно расширить возможности базы данных. В качестве мысленного упражнения полезно представить этот раздел без рисунков в виде многоуровневой строки с разделителями. Какие разделители выберете?
Очевидно, для строк с разделителями должны поддерживаться следующие операции:
В Cache ObjectScript для решения этих задач используются функции $LENGTH, $FIND, $EXTRACT, $PIECE.
Итак, выбираем в качестве разделителя печатаемый символ, который не может использоваться в тексте полей, скажем: """, "+" и т. д. Строка с разделителями создаётся обычным присваиванием (команда "SET"). Создадим две строки str1="A~77~BD" и str2="A+B~A/B~A B" (листинг 3.23),
определим их длины (не в символах, а в полях) и выделим поля со второго по третье в строке str1.
USER>S str1="A^77^BD" USER>S str2="A+B^A/B^A B" USER>W "Длина str1=",$L(str1,"^") Длина str1=3 USER>W "Длина str2=",$L(str2,"^") Длина str2=3 USER>W $P(str1,"^",2,3) 77^BD
Количество полей определяется уже известной функцией $L[ENGTH] , в которой во втором аргументе записывается знак разделителя. Выделяется нужное поле функцией $PIECE, имеющей формат:
$$$P[IECE](строка, разделитель, первое_поле,последнее\_поле)$$
Например, функция $P(str,"^",1) выделяет первое поле, а $P(str, "^",1,3) — поля с первого по третье. Функция $PIECE относится к так называемым левым функциям. Это означает, что она может стоять в левой части команды присваивания. Вставим текст "QQ" на место второго поля, добавим четвёртое поле и седьмое поля (листинг 3.24).
USER>S str1="A^77^BD" USER>S $P(str1,"^",2)="QQ" w str1 A^QQ^BD USER>s $P(str1,"^",4)="PP" w str1 A^QQ^BD^PP USER>s $P(str1,"^",7)="PP" w str1 A^QQ^BD^PP^^^PP
Заметим, что вставка в седьмое поле привела к созданию пустых пятого и шестого полей "A^QQ^BD^PP^^^PP", что и вызвало появление в записи строки двух дополнительных циркумплексов.
Одна из основных структур данных — список — создаётся специальной функцией $LISTBUILD (сокращенно $LB). Её формат:
$$$LISTBUILD(элeмeнт\_спискa [,элемент_списка ...])$$
Создадим список из двух элементов и с помощью команды ZZDUMP разберёмся с его структурой (листинг 3.25).
USER>S x=$LB("Red","Blue")
USER>ZZDUMP x
0000: 05 01 52 65 64 06 01 42 6C 75 65
В левой части строки коды символов, а в правой отпечатанная строка. Точками обозначены непечатаемые символы. Оказывается, элементы списка разделяются парами непечатаемых символов. Первый символ такой пары — это число в шестнадцатиричной системе счисления, равное количеству символов в следующем за разделителем элементе списка плюс 2. Вторым идёт символ с кодом 01. Разделители в конце списка не ставятся. Зададим список из одного пустого элемента и список из единственного элемента, представляющего пустую строку (листинг 3.26). Пустой список эквивалентен пустой строке
USER>S y=$LB(), z=$LB("") ZZDUMP x,y
0000: 01
0000: 02 01
При вставке в список значения не определённой переменной ошибка не появляется, а вставляется NULL (листинг 3.27).
USER>K a S list1=$LB(1,a,3) USER>F i=1:1:3 W "i=",i,?10,$LISTGET(list1,i),! i=1 1 i=2 i=3 3
Двухаргументная функция $LISTGET была использована для извлечения из списка указанного первым аргументом элемента с номером, указанным вторым аргументом.
Результат конкатенации двух списков есть список. Для работы со списками кроме их создания необходимо определять правильность списков (функция $LISTVALID) и их длину (функция $LISTLENGTH), сравнивать списки ($LISTSAME). Навигация по спискам производится с использованием функций $LISTDATA, $LISTFIND, $LISTGET, $LISTNEXT. Предусмотрены преобразования строк с разделителями в списки (функция $LISTFROMSTRING) и обратное преобразование ($LISTTOSTRING).
Булева функция $LISTVALID проверяет правильность списка. Для правильных списков возвращает 1, а для неправильных 0. Формат:
$$\$LISTVALID(вырaжeниe)$$
или
$$\$LV(вырaжeниe)$$
Проверяемый список должен быть создан с помощью функций $LIST-BUILD и $LISTFROMSTRING, либо извлечён из существующего списка с помощью $LIST. В листинге 3.28 приведено несколько примеров, достаточных, чтобы разобраться с деталями.
USER>S r="A",s=33,t="",u=$LB("A","B"),v=$LB(1)
USER>W $LV(r),?10,$LV(s),?20,$LV(t)
0 0 1
USER>W $LV(u),?40,$LV(v)
1 1
USER>S y=$LB(NULL) w $LV(y)
1
Вы видите, что пустой элемент t есть правильный список.
Возвращает длину списка. Неопределённые элементы засчитываются. Вложенные списки учитываются как один элемент.
$$\$LISTLENGTH(список)$$
или
$$\$LL(список1)$$
Проверяет совпадение списков. Формат:
$$\$LISTSAME(список1, список2)$$
или
$$\$LS(список1, список2)$$
Примеры употребления приведены в листинге 3.29.
USER>s x=$LB("A","B"), y=$LB("B","A"), z=$LB("A","B"), v=$LB(), w=$LB("")
USER>w "$LS(x,y)="_$LS(x,y),!,"$LS(x,z)="_$LS(x,z),!,"$LS(v,w)="_$LS(v,w),!
$LS(x,y)=0
$LS(x,z)=1
$LS(v,w)=0
Обратите внимание на то, что пустая строка (в примере в листинге 3.28) и список из одного пустого элемента (x) оба допустимые списки, но они не равны между собой.
Формат:
$$\$LISTDATA(список, позиция)$$
или
$$\$LD(список, позиция)$$
Функция $LISTDATA проверяет указанный элемент списка и возвращает единицу, если элемент в такой позиции существует и имеет значение. Если нет такой позиции или значение элемента не определено, то вернётся ноль (листинг 3.30)
USER>k s x=$LB("Один",,y,"","Пять")
USER>s pos="Позиция "
USER>f i=0:1:6 w pos,i,?15,"$LD(x,",i,")=",$LD(x,i),!
Позиция 0 $LD(x,0)=0
Позиция 1 $LD(x,1)=1
Позиция 2 $LD(x,2)=0
Позиция 3 $LD(x,3)=0
Позиция 4 $LD(x,4)=1
Позиция 5 $LD(x,5)=1
Позиция 6 $LD(x,6)=0
В $LISTDATA, как в остальных функциях для работы со списками, "список" —это выражение, дающее имя существующего списка. Если такого списка нет, возникает ошибка UNDEFINED.
Функция $LISTFIND это наша старая знакомая $FIND приспособленная для работы со списками. Она ищет первое вхождение указанного значения в качестве элемента списка. Совпадение должно быть точным. Формат:
$$\$LISTFIND(список, значение, начальная\_позиция)$$
или
$$\$LISTFIND(список, значение)$$
Если элемент найден, возвращается его номер, а если не найден, вернётся 0 (листинг 3.31).
USER>S x=$LB("A","BB","B","C")
USER>W "$LF(x,""A"")=",$LF(x,"A")
$LF(x,"A")=1
USER>W "$LF(x,""B"")=",$LF(x,"B")
$LF(x,"B")=3
USER>W "$LF(x,""BB"")=",$LF(x,"BB")
$LF(x,"BB")=2
USER>W "$LF(x,""D"")=",$LF(x,"D")
$LF(x,"D")=0
Прикрепление пустого элемента, как к голове, так и к хвосту списка, не изменяет список (листинг 3.32).
USER>ZZDUMP x 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C USER>ZZDUMP ""_x 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C USER>ZZDUMP x_"" 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C
Необходимо помнить, что сравнение выполняется после приведения данных к так называемой канонической форме. Для списка созданного командой
S y=$LB("1.0","+2","003","2*2")
все следующие функции возвращают 0 потому, что числа не приводятся к текстовым константам: $LF(y,1.0), $LF(y,+2), $LF(y,003), $LF(y,4). А для списка созданного командой S y=$LB(7,6) обе следующие функции возвращают 1 потому, что вторые аргументы этих функций согласуются с элементами списка: $LF(y,7.000), $LF(y, 006).
Возвращает элемент списка в указанной позиции или значение по умолчанию, если этот элемент не определён. Формат:
$$\$LISTGET(список, позиция [, значение\_по\_умолчанию])$$
или
$$\$LG(список, позиция [, значение\_по\_умолчанию])$$
В листинге 3.33 показан пример использования функции $LISTGET и сравнение её с функцией $LIST, которая выдаёт ошибку при появлении неопределённого элемента.
USER>S y=$LB("A",,"B")
USER>F i=1:1:3 W "Элемент ",i,?12,$LG(y,i,"null"),!
Элемент 1 A
Элемент 2 null
Элемент 3 B
USER>F i=1:1:3 W "Элемент ",i,?12,$LI(y,i),!
Элемент 1 A Элемент 2
F i=1:1:3 W "Элемент ",i,?12,$LI(y,i),!
^
<NULL VALUE>
USER>
Извлекает элементы списка последовательно. Работает очень быстро. Формат:
$$\$LISTNEXT(список, указатель, значение)$$
Локальная переменная "указатель" перед первым вызовом функции должна быть установлена в 0. Тогда список будет анализироваться с начала.
Локальная переменная "значение" не может быть массивом. Инициализировать её не следует. Функция будет помещать в неё значения элементов списка (листинг 3.34).
USER>S y=$LB("A","B","C"), p=0
USER>F i=1:1:10 W "i=",i,?6,"Ф=",$LISTNEXT(y,p,v),?10,"p=",p,?20,"v=",v,!
i=1 Ф=1 p=589827 v=A
i=2 Ф=1 p=589830 v=B
i=3 Ф=1 p=589833 v=C
i=4 Ф=0 p=0 v=C
i=5 Ф=1 p=589827 v=A
i=6 Ф=1 p=589830 v=B
i=7 Ф=1 p=589833 v=C
i=8 Ф=0 p=0 v=C
i=9 Ф=1 p=589827 v=A
i=10 Ф=1 p=589830 v=B
Когда $LISTNEXT достигнет конца списка, она вернёт 0, переустановит указатель в 0 и оставит предыдущее извлечённое "значение". Продолжится анализ списка с начала. Элементы вложенных списков не выделяются. Удобно использовать функцию с циклом WHILE. Проверьте командную строку: S p=0 WHILE $LISTNEXT(y,p,v) {W !,v}.
Создаёт список из строки с разделителями. Формат:
$$\$LISTFROMSTRING(строкa, ограничитель) $$
или
$$\$LFS(строкa, ограничитель)$$
Пример приведен в листинге 3.35.
USER>S string="A^B^C", list=$LFS(string,"^") USER>ZZDUMP list,string 0000: 03 01 41 03 01 42 03 01 43 ..A..B..C 0000: 41 5E 42 5E 43 A^B^C USER>
Преобразует список в строку с разделителями. Если разделитель не указан, выбирается запятая. Отсутствие флага или нулевое его значение определяет появление ошибки <NULL VALUE>. Формат:
$$\$LISTTOSTRING(список[,огрaничитeль][,флaг])$$
или
$$\$LTS(список[,огрaничитeль][,флaг])$$
Обратите внимание на то, что функция $LTS не проверяет наличие в исходном списке разделителя, который будет использован в преобразовании. Поэтому в последнем примере в листинге 3.36 ошибочно получена строка из трех элементов с разделителем "пробел".
USER>S x=$LB("A","B"), y=$LTS(x,"разделитель")
USER>ZZDUMP y
0000: 0041 0440 0430 0437 0434 0435 043B 0438 Aраздели
0008: 0442 0435 043B 044C 0042 тельB
USER>S z=$LB("A B","C")
USER>S y=$LTS(z," ")
USER>ZZDUMP y
0000: 41 20 42 20 43 A B C
USER>
Программа (routine) — это, как всегда, именованный блок кода, предназначенный для выполнения некоторого набора операций. Имя программы не должно содержать знаков подчеркивания ("_"), тире ("-") и точки с запятой (";").
Используются три расширения имени .mac, .inc и .int. Исходный текст программы, написанной на языке Cache Basic имеет расширение .bas.
Файл исходного кода с расширением .mac обрабатывается макропрепроцессором, который транслирует макросы и встроенные SQL и HTML, получая файл с расширением .int. А этот файл транслируется в исполняемый объектный код с расширением obj. Тексты HTML используются в серверных страницах, которые в этой книге не рассматриваются.
Создавать программу можно в любом текстовом редакторе, но лучше воспользоваться Студией, с которой мы уже познакомились.
Кроме определённых в разделе 3.2.2 простейших командных строк, состоящих из команд записанных через пробел, в программах используются другие типы командных строк:
По поводу меток необходимо сделать несколько разъяснений. Во-первых, метки могут быть общедоступными (public) и внутренними (private). По умолчанию устанавливается значение public. Такая метка может быть вызвана из любой программы, а метка private — только из текущей программы.
Во-вторых, метки можно снабдить списком формальных параметров, заключённым в круглые скобки. Подпрограмма, которая будет вызвана с такой метки, воспримет переданные параметры.
В зависимости от того, возвращает ли программа значение, вызывать её из другой программы или терминала можно двумя способами, командой D[O] или помещением в правую часть присваивания (рисунок 3.18)
(рис 3.18) Два способа вызова программы-функции
Программа может содержать одну или несколько меток. Одна из них может помещаться в самом начале программы перед первой командой. Уже упоминалось, что имя метки пишется в Студии с первой позиции, в отличие от команд, перед которыми помещается минимум один пробел. Простейшая программа приведена на рисунке 3.16..
Тексты двух программ с метками и с вызовом программы приведены на рисунке 3.19. В программе MyProc1 в первой строке указано, что метка снабжена параметрами MyProc1(x,y), а переменная z заключена в квадратные скобки, указывающие, что она глобальная. В терминале видно, что программа MyProc1 просто создала локал z, а переменные x и y не доступны вне MyProc1.
(рис 3.19) Программы и метка public
Существует некоторая "нелогичность" в записи вызова по метке с параметрами. Следует писать фактические параметры после имени программы:
$$имя\_метки\ \hat{}\ имя\_программы(фактические\_параметры)$$
По умолчанию сама программа определяется как public. Вариант private смысла не имеет. Программа может содержать в себе подпрограммы, процедуры и функции.
Процедура это именованный блок кода программы, имеющий формат:
$$имя\_процедуры(формальные\_параметры)\verb*| |[общедоступные\_переменные] \verb*| |доступ\verb*| |{код\_тела\_процедуры}$$
Доступ определяет, можно ли вызывать процедуру из других программ. Это либо слово private, либо необязательное ключевое слово public (общедоступная).
Процедура начинается с метки-имени процедуры, за которым могут следовать список формальных параметров и список общедоступных переменных.
Все метки внутри процедуры имеют тип private и могут использоваться только внутри процедуры. Указание слова public вызовет ошибку.
Команда GOTO внутри процедуры может ссылаться только на внутренние её метки. Перед закрывающей фигурной скобкой выполняется неявная команда QUIT. Косвенность и команда XECUTE выполняются как внешняя по отношению к процедуре программа. Все переменные процедуры, за исключением описанных в списке общедоступных переменных, внутренние. Пример процедуры уже рассмотрен нами (рисунке 3.19).
Функции бывают внутренние и внешние. Внутренняя функция —это размещённая внутри программы процедура, возвращающая значение. Для этого в команде завершения после QUIT записывают выражение, значение которого возвращается, например, Q x+y, как в программе на рисунке 3.18.
Итак, функция отличается от подпрограммы только способом завершения. Для этого в теле функции помещается команда
$$quit\ выражение$$
Для вызова внутренней функции достаточно указать её имя. При вызове внешней функции перед именем помещается два знака доллара.
Пример: процедуру, описанную следующим образом в программе:
^demofunc sum(a,b) W "Вычисляем сумму "_а_"и"_b,! Q a+b
можно вызвать и как процедуру
USER>DO sum^demofunc(1,2) Вычисляем сумму 1 и 2
и как функцию — с возвратом значения
USER>WRITE $$sum^demofunc(1,2) Вычисляем сумму 1 и 2 3
Вызовы по ссылке и по значению понимаются в обычном смысле. При вызове по значению для формальных параметров в начале исполнения вызываемой программы выполняется неявная команда NEW, а по завершении её неявная команда KILL. Поэтому формальные параметры существуют только в вызываемой программе.
При вызове по ссылке вызываемая программа получает указатель на переменную вызывающей программы. Поэтому любое действие в вызываемой программе — присваивание и даже удаление командой KILL — влияет на эти переменные.
Имя переменной вызываемой по ссылке отмечается точкой перед именем.
Два важных замечания:
(рис 3.20) Вызов по ссылке
Заметим, что в последнем примере при вызове по ссылке с неопределённым фактическим параметром .var после выхода из подпрограммы образовался локал с древесной структурой var, var(1), var(2), var(1,1), копирующий структуру локала x заданного в подпрограмме.
Вспомним, что в ObjectScript используется четыре типа файлов программ с расширениями имён .mac, .inc, .int и .obj. Исходный текст программы (макрокод) помещается в файл с расширением .mac.
Файлы .inc содержат программы, включаемые в основную программу .mac с помощью команды include, что определило расширение имени. Основное назначение — построение макробиблиотек.
Файл .mac компилируется в файл промежуточного кода с расширением .int, а затем в исполняемый код, помещаемый в файл .obj. Для исполнения программы используется только объектный код.
Формат команды include:
$$\verb*|#Include имя|\_файла$$
Исполняемая программа загружается в пространство адресной памяти, называемое разделом. Промежуточный код, представляемый как файл с расширением .int хранится в глобале ^ROUTINE, находящемся в том же пространстве имён, что и сама программа. Структура глобала:
^ROUTINE("имя программы",0,0) = количество строк
^ROUTINE("имя программы",0,1) = первая строка
^ROUTINE("имя программы",0,2) = вторая строка и
т.д.
Остальные типы программ тоже размещаются в глобалах: .mac в ^rMAC, .inc в ^rINC, .obj в ^rOBJ. Структура файла ^rMAC такая же, как у ^ROUTINE . При вызове программы по имени без расширения сначала ищется файл .mac, а затем .int. Хранятся старые версии файлов .mac и .inc. Предпоследние версии имеют расширения .mac.1 и .int.1. У более старых версий расширения .mac.2 и .int.2.
Если, например, командой SET изменить узлы ^ROUTINE, то объектный код не будет совпадать с исходным текстом. Однако, Cache не пытается синхронизировать версии .int, .mac, и .obj. Только после новой компиляции они будут соответствовать друг другу.
Для чтения исходных текстов будем использовать функцию $T[EXT]. Её синтаксис:
$$\$TEXT(мeтка+смeщeниe\ \hat{}\ программа)$$
Можно использовать косвенность $TEXT(@выражeниe). Функция возвращает строку исходного текста программы, хранящуюся в указанном месте.
Разберитесь с текстами программ (рисунок 3.21), одна из которых распечатывает свой текст (SelfOutput.mac), а вторая (Text.mac) может распечатать текст любой программы.
(рис 3.21) Программы, распечатывающие программы
В ObjectScript программа может изменять себя и другие программы. Основные команды, участвующие в этом процессе, удобно рассматривать на основе имеющейся несколько архаичной возможности создания и изменения программы непосредственно из терминала. Введём в терминале несколько командных строк, начиная их знаком табуляции:
USER><TAB> командная_строка … USER><TAB> командная_строка
Затем без символа табуляции наберём команду сохранения и компиляции программы
$$USER>ZSAVE \verb*| |имя\_программы$$
Ниже приведен пример ввода программы в терминале и её исполнения.
USER> W "Исполнена первая строка",! USER> W "Исполнена вторая строка",! USER>ZSAVE test USER>D ^test Исполнена первая строка Исполнена вторая строка USER>S y="ZINSERT "" W 77"":+2" USER>X y USER>D ^test Исполнена первая строка Исполнена вторая строка 77
Вставка в программу в качестве третьей строки команды W 77 выполнена с использованием XECUTE для предварительно сформированной строки, в которой определён аргумент в виде команды, которая будет вставлена "w 77" и задано смещение от начала программы +2. Это ZINSERT "W 77":+2.
Команда
$$\verb*|ZR[EMOVE] смещение|$$
удаляет строку.
С помощью Студии проверьте существование этой программы. Учтите, что она создаётся с расширением .int.
Для работы с текстами программ используют ещё две команды:
Удаление программы из раздела $$\verb*|ZR[EMOVE] имя|$$
загрузка программы в раздел $$\verb*|ZLOAD имя|$$
Замечание. Для практических целей следует использовать программу ^%r и класс %Library.Routine, которые работают гораздо быстрее.
В COS имеется уникальная возможность измерения промежутков времени с разрешением по-видимому до нескольких микросекунд. С этой целью используется функция $ZH, которая фиксирует временную метку:
USER> w $zh 3819.544684
Первая часть числа 3819 —это число секунд от включения машины до времени исполнения $ZH, а вторая часть это дробные доли секунды.
Если отметить время до и после выполнения команды, командной строки или другой конструкции, то разность временных меток даёт длительность процесса.
Значительные разбросы результатов получаются из-за того, что невозможно остановить процессы операционной системы, не связанные с исполнением нашей программы. Поэтому для получения достоверных результатов необходимо многократно повторять измерения и усреднять оценки.
Исполняя программу ^zhtime.mac (листинг 3.37) при одном и том же числе повторений и при разном их числе можно прийти к выводу о том, что единичное исполнение функции $ZH, даёт большие разбросы, а при числе повторений более 105 получаются стабильные результаты (таблица 3.2).
r "Введите число повторений: ", N
s time=0
f i=1:1:N {
s t0=$zh,t1=$zh,dt=t1-t0,time=time+dt
}
s time=time/N
w !,"Время исполнения $zh = ",time*10e6, " мкс"
q
| Повторов | 1 | 10 | 100 | 1000 | 10000 | 10**5 | 10**6 | 10**7 | 2*10**7 |
| Время (мкс) | 7 | 2,6 | 2,25 | 2,23 | 2,43 | 2,33 | 2,35 | 2,34 | 2,34 |
На моей машине в Windows XP время между двумя последовательно выполняемыми засечками времени составляет около 2,34 мкс.
Как показывают результаты экспериментальной проверки, полученное время между исполнениями $ZH не следует вычитать из общего времени, полученного в результате применения конструкции вида
$$s\verb*| |t0=$ZH\verb*| |исследуемая\_структура\verb*| |s\verb*| |t1=$ZH, dt=t1-t0$$
Это было бы необходимо, если бы задержка происходила только в результате исполнения программы. По-видимому, задержка определяется в основном ограниченными возможностями аппаратной части компьютера, не обеспечивающей более частые обращения к средствам хранения системного времени.
Для измерений длительности выполнения созданной вами программной конструкции вставьте её между командами S t0=$ZH и S t1=$ZH в программу в (листинге 3.37.
Теперь, изучив ObjectScript, мы может заняться организацией иерархических баз данных в Cache. Поскольку запросы в них имеют навигационный характер, необходимо освоить команды, позволяющие определить свойства узлов, организовать поиск в ширину и глубину.
Дополнительно будет рассмотрена команда копирования индексированных переменных, которая упростит групповые вставки.
Представим набор записей в виде массива, в котором имя массива — это имя набора, а имена индексов это имена полей записи. Значениями индексов в такой модели хранения данных будут значения полей. Если хотя бы один домен имеет мощность континуума, например, домен с вещественными числами, то ограничиться целочисленными индексами, как в массивах, изучаемых в математике, не удастся.
В языках общего назначения при определении массива с конечным набором значений индексов необходимо задать его размерность. Это необходимо, чтобы выделить область памяти, достаточную для хранения массива. Такие традиционные массивы будем называть плотными. В практически важных моделях данных такой массив сильно разрежен. Большую часть выделенного объёма занимают пустые места, соответствующие комбинациям значений индексов, которые не могут быть использованы или ещё не использованы.
Разреженные массивы, реализованные в Cache, отличаются следующими особенностями:
Вторая ипостась разреженного многомерного массива — это дерево. Нетрудно догадаться, что если присутствуют не все узлы, необходимые для образования дерева, то, может быть, придётся вводить в рассмотрение так называемые виртуальные узлы.
Рассмотрим пример. Создадим дерево в виде локала из следующих узлов: a(1)="A", a(2)="B", a(1,1,1)="C". В узле a(1)="A" единица —это индекс, "А" — значение узла a(1). Строго говоря, дерево получается (рисунок 3.22), только если считать, что к заданным узлам добавлен корневой узел a="" (его значение пустое, но могло быть любое другое) и промежуточный узел a(1,1). Последний называется виртуальным узлом. Такие узлы имеют имя и индексы, но не имеют значения. Их предназначение — связать узлы, отличающиеся на два и более индексов, чтобы на каждом следующем уровне дерева добавлялся ровно один индекс. Виртуальные узлы будем изображать двумя концентрическими окружностями.
(рис 3.22) Виртуальный узел а(1,1)
Не думайте, что виртуальные узлы вводятся в угоду теории. В следующем разделе станет понятно, что без них невозможна навигация по деревьям.
Замечание. Вы, конечно, заметили, что мы следуем противоестественной привычке математиков высаживать деревья вверх корнями.
Набор деревьев (лес) всегда можно свести в одно дерево или небольшое их число. На рисунке 3.23 представлены в экземплярах две связанные сущности "Команда" и "Игрок", ранее рассматривавшиеся в разделе 2.2.7 (рисунок 2.19).
Сущность "Команда" с экземплярами представлена на рисунке 3.23 деревом глубины 2. Заметим, что можно было бы ограничиться деревом глубины 1. В этом случае в нем на уровне 1 существовали бы узлы вида ^Команда (1) =" 1 "Спартак" или ^Команда (1)=$lb(" 1","Спартак") или что-нибудь ещё.
(рис 3.23) Представление двух связанных сущностей глобалами: Данные
Позже вы увидите, что именно так хранятся строки таблиц в реализации реляционной модели.
В варианте, представленном на рисунке 3.23рисунке 3.50, данные каждой сущности хранятся в своём дереве, а для всех метаданных использовано дополнительное дерево (рисунке 3.24). Поясним значения листовых узлов. РК—первичный ключ, NK —не ключ, FK—внешний ключ. Значения узла лМетадан-ные ( "Связь", "Связь1" ) представлено строкой с разделителем """
(рис 3.24) Представление двух связанных сущностей глобалами: Метаданные
Можно объединить все деревья в одно, выполнив необходимые переименования (рисунок 3.25)
(рис 3.25) Представление базы данных сущностей одним глобалом
Кроме констант и простых переменных узел может принимать следующие значения:
строка с разделителями, например,
K ^a S ^a(1)="a%b%c" W ^a(1)
список, например,
K ^a S ^a(1,1,1)=$LB("a","b")
W ^a(1,1,1),"~",$P(^a(1,1,1),2)
значение узла другого массива, например,
K ^b S ^b(1)="b1", ^b(2)="b2", ^b(1,1)="b11" S ^a(1,1,1)=^b(1) W ^a(1,1,1)
В соответствии с известным выражением, вы вольны расширять глобал в любую сторону своей души. Это не означает, конечно, что все варианты будут работать одинаково эффективно.
В результате создана база, содержащая данные и метаданные, организованные в иерархии. Система управления базой отсутствует. Все манипуляции с данными и метаданными и запросы данных придётся выполнять врукопашную. Контроль ограничений целостности не организован. Связь обозначена, но необходимо дописать процедурную часть, обеспечивающую её работу.
В иерархической модели данных для работы с базой необходимо, прежде всего, обеспечить навигацию по данным. Это означает, что всегда можно определить, в какой структуре, в каком элементе данных находится система управления базой и как попасть в нужное место. Для программной навигации необходимо хранить метаданные в словаре. В примерах предыдущего раздела метаданные хранились в глобалах, а процедурная часть словаря проработана не была.
В деревьях Cache навигация осуществляется на физическом уровне. Необходимо обеспечить движение в ширину (функция $ORDER) и движение в глубину (функция $QUERY). При конструировании глобалов их узлы создаются и удаляются командами SET и KILL. Копирование поддеревьев обеспечивает команда MERGE.
Существуют функции, поставляющие дополнительную информацию, необходимую для обеспечения навигации:
$DATA), сколько индексов у переменной (функция $QLENGTH);$QSUBSCRIPT);$GET)Начнём с получения вспомогательных данных.
Узлы можно характеризовать двумя признаками — наличием потомков и наличием значений. Если у узла нет потомков, но есть значение, то это листовой узел (на рисунке 3.22 — узлы a(2), a(1,1,1)). Если у узла есть потомки и значения, то это либо корень дерева, либо реальный промежуточный узел (узлы a, a(1)). Узел, имеющий потомков, но не имеющий значений — виртуальный (узел a(1,1)).
Тип узла определяет функция $DATA. Её значения сведены в таблицу 3.3
| Потомки | Значение узла | Код | Значение $DATA |
| 0 | 0 | 00 | 0 |
| 0 | 1 | 01 | 1 |
| 1 | 0 | 10 | 10 |
| 1 | 1 | 11 | 11 |
Cache не печатает ведущие (стоящие слева) нули. Поэтому возвращаются значения $DATA такие, как указано в последнем столбце таблицы.
В следующую таблицу 3.4 сведены варианты условий, проверяемых функцией $DATA.
| Условие | Значение $DATA | Запись условий |
| Потомки имеются | 10,11 | $DATA(узел)\10 |
| Значения имеются | 1,11 | $DATA(узел)#10 |
| Узел не существует | 0 | $DATA(узел)=0 |
| Узел виртуальный | 10 | $DATA(узел)=10 |
| Узел существует | 1,10,11 | $DATA(узел) |
Создадим локал с древесной структурой и проверим типы узлов с помощью функции $DATA (листинг 3.38).
USER>S a="", a(1)="A", a(2)="B", a(1,1,1)="C" USER>W $D(a),?10,$D(a(1)),?20,$D(a(2)),?30,$D(a(1,1,1)) 11 11 1 1 USER>W $D(a(1,1)) 10
Уровень узла, то есть число индексов в записи узла определяется функцией $QL[ENGTH]. Корень дерева имеет уровень 0 (у него нет индексов), на уровне 1 находятся узлы с одним индексом и т. д.
Для только что введенного дерева, просмотрим уровень всех узлов (листинг 3.39).
Функция $QL не просматривает реального узла, а только изучает текст своего аргумента. Поэтому можно определить глубину несуществующего узла несуществующего дерева (листинг 3.39рисунок 3.54, последняя командная строка).
USER>S a="", a(1)="A", a(2)="B", a(1,1,1)="C"
USER>W $QL("a"),?10,$QL("a(1)"),?20,$QL("a(1,1)")
0 1 2
USER>W $QL("a(1,1,1)")
3
USER>K W $QL("a(2,5)")
2
Функция $QS[UBSCRIPT] работает с полным именем переменной, в том числе извлекает индексы.
Полное имя включает имя области имён, которое окружается символами "|" и "|". Записывают его между знаком "^" и именем переменной с индексами (листинг 3.40).
Частные случаи команды:
$QS("имя",-1) возвращает окружение, если оно есть в полном имени;$QS("имя",0) возвращает имя;$QS("имя",n), где n > 1 возвращает индекс, если он есть, и пустую строку, если его нет.
USER>S ^a(1)=77
USER>ZN "%SYS"
%SYS>S a=^|"USER"|a(1) w a
77
%SYS>W $QS("^|""USER""|a(1)", -1)
USER
%SYS>W $QS("^|""USER""|a(1)", 0)
^a
%SYS>W $QS("^|""USER""|a(1)", 1)
1
%SYS>W $QS("^|""USER""|a(1)", 2)
%SYS>
Функция $G[ET] (аргумент) возвращает значение узла (листинг 3.41). Если аргумент отсутствует, вернётся пустая строка.
USER>K y S a="0" a(1)="B" USER>W "a=",$G(a),?10,"a(1)=",$G(a(1)),?20,"a(y)=",$G(y) a=0 a(1)=B a(y)=
Почти все команды или функции, обращающиеся к глобалам, изменяют указатель, называемый индикатором неполной глобальной ссылки. Он запоминает предка последнего узла, к которому производилось обращение.
В программе неполная глобальная ссылка записывается как знак """, за которым в круглых скобках следуют индексы, которые должны быть добавлены к индексам, хранящимся в индикаторе.
Примеры использования неполной глобальной ссылки приведены в листинге 3.42.
USER>S ^a="",^a(1)="A",^a(2)="B",^a(1,1)="C" USER>S ^a(1,1,1)="D",^a(1,1,2)="E" USER>S x=^a(1) w ^(2) B USER>S x=^a(1,1,1) w ^(2) E
Неполная глобальная ссылка становится не определённой после ссылки на неиндексированную переменную и после вызова функции $QUERY, которая будет описана в следующем разделе. Там же станет понятно, что неполная глобальная ссылка хорошо работает вместе с функцией $ORDER.
Последняя функция $NA[ME] возвращает имя с вычисленными индексами. Более точно,
$$\$NA(имя(i_1,...i_n), m)$$,
где $$m$$ —число, вернёт:
при $$m=0$$ —только имя без индекса;
при $$m>0, m \leq n$$ —имя с n индексами;
при $$m<0$$ —ошибка;
при $$m>n$$ —то же, что $$m=n$$.
Следует помнить, что обращения к узлу не происходит. Он может вообще не существовать.
Обратите внимание на то, что глобал при записи сокращается за счёт удаления повторяющихся значений индексов. Рассмотрим пример из документации Cache. Глобал:
^Data(1999) = 100 ^Data(1999,1) = "January" ^Data(1999,2) = "February" ^Data(2000) = 300 ^Data(2000,1) = "January" ^Data(2000,2) = "February"
будет храниться примерно в таком виде:
Data(1999):100|1:January|2:February|2000:300|1:January| 2:February|...
Такая запись может уменьшить количество читаемых блоков памяти.
Оба вида поиска в этом разделе будут демонстрироваться на глобалах. Однако всё сказанное применимо и к локальным многомерным массивам.
В разреженных массивах практически никогда нельзя предугадать следующий индекс на выбранном уровне индексации. Точно также не известны ни предыдущий индекс, ни первый, ни последний индексы на каждом уровне индексации.
Функция $O[RDER] (сокращённо $O) использует в качестве аргумента узел дерева и возвращает значение индекса следующего по порядку элемента. Первый аргумент даёт имя локала или глобала. Второй аргумент указывает направление обхода. Значение +1, оно же значение по умолчанию, определяет движение по возрастанию. Значение —1 задаёт движение по убыванию.
$$\$O[RDER](имя\_переменной, направление\_обхода)$$
Создадим глобал, изображённый на рисунке 3.26.
s ^G="",^G(-1)=-1,^G(1)=1,^G(7)=7,^G(-1,5)=-15 s ^G(-1,10)=-110,^G(7,"A")="7A",^G(7,"B")="7B"
Обратите внимание, что, например, команда W $O(^G(""),1)
вернёт индекс —1 крайнего левого узла на глубине 1. Команда $O(^G(7),1) вернёт пустой индекс.
Можно считать, что цепочка индексов каждого горизонтального подуровня замыкается на фиктивный пустой индекс, что изображено на на рисунке 3.26.
(рис 3.26) Функция $ORDER
Начнём движение с второго пустого индекса. Команда W $O^G(7, ""),-1) вернёт индекс крайнего правого узла в правом горизонтальном слое уровня 2. После выполнения команд W $O^G(7,"B"),-1) и $O^G(7,"A"),-1) получим пустой индекс. Цепочка замкнулась.
Для поиска самого левого индекса воспользуйтесь следующими функциями:
W $O^G(""),1) даёт крайний левый узел на первом уровне дереваW $O(^G(""),-1) даёт крайний правый узел на первом уровне дереваW $O(^G(-1,""),1) даёт крайний левый узел на втором уровне поддерева, образованного узлом ^G (-1).Обойти все узлы поддерева на одном уровне можно с помощью циклов, учитывающих имя узла, образующего поддерево, и замкнутость горизонтальных подуровней на узел с пустым индексом (листинг 3.43)
USER>S x="" F S x=$O(^G(x)) Q:x="" W !, x, ?30, ^G(x) -1 -1 1 1 7 7 USER>S x="" F S x=$O(^G(7,x)) Q:x="" W !,x,?30,^G(7,x) A 7A B 7B
Не забывайте, что после ключевого слова F в цикле без параметров и перед командой W стоят два пробела.
$0RDER пробегает все узлы уровня, включая виртуальные. Однако, приведенный цикл использовать без переделки нельзя, потому что, встретив виртуальный узел, не имеющий значения, он не сможет распечатать его.
Уточним детали. Сортируемые элементы располагают слева направо в порядке возрастания.
Вспомним естественный порядок сортировки. Сначала канонические числа, не содержащие избыточных нулей и знака "+", в порядке возрастания1. Затем неканонические числа. За ними идут слова, начинающиеся с букв латиницы. После них слова, начинающиеся с букв кириллицы.
Для уточнения порядка сортировки одиночных символов наберите следующие две строки
F i=1:1:255 S a($CHAR(i))=$CHAR(i) S x="" F S x=$O(a(x)) Q:x="" W !,x,?30,a(x)
и сами рассмотрите полученную последовательность. Обратите внимание на то, что буквы кириллицы расположены не подряд.
Замечание. Функция $CHAR(i) выдаёт символ с кодом i.
Поиск в глубину производится с помощью функции $Q[ERY], возвращающей в отличие от $0RDER, не следующий индекс, а имя следующего узла. Однако, возвращаются лишь узлы имеющие значение, то есть виртуальные узлы игнорируются. Движение по ним производится, но результаты не выдаются.
При обходе всего глобала начинаем движение с корня. Правила перемещения:
При этом необходимо помнить, что движение по горизонтали, как и для $0RDER возможно только по узлам одного подуровня, имеющим общего непосредственного предка.
Для проверки правила несколько изменим глобал ^G, добавив в него узел ^G(1,1,1) так, чтобы образовался виртуальный узел ^G(1,1) (листинг 3.44)
USER>S ^G="", ^G(-1)=-1, ^G(1)=1, ^G(7)=7, ^G(-1, 5)=-15 USER>S ^G(-1, 10)=-110, ^G(7, "A")="7A" USER>S ^G(7, "B")="7B", ^G(1,1,1)=111 USER>S x="^G" F S x=$Q(@x) Q:x="" W x, "=", @x, ! ^G(-1)=-1 ^G(-1, 5)=-15 ^G(-1, 10)=-110 ^G(1)=1 ^G(1,1,1)=111 ^G(7)=7 ^G(7, "A")=7A ^G(7, "B")=7BF
После узла ^G(1) был пройден виртуальный узел ^G(1,1), но поскольку он не имеет значения, в выходных данных сведений о нём нет. При следующем повторе цикла отработано движение вниз к узлу ^G(1,1,1).
Мнемоническое изображение правил поиска в глубину с использованием функции $QUERY и подробное обписание обхода дерева ^G из предыдущего примера приведены на рисунке 3.27.
(рис 3.27) Функция $QUERY
Команда MERGE позволяет вклеивать в индексированные переменные копии других индексированных переменных, независимо от того, являются ли они локальными или глобальными. Синтаксис похож на синтаксис присваивания:
$$M[ERGE]\verb*| |переменная\_результат = вставляемая\_переменная$$
Для проверки результатов этой операции удобно воспользоваться программой просмотра глобала ^%G (листинг 3.45). При первом её исполнении в ответ на предложение ввести устройство и ширину строки нажимайте на Enter, вставив значения по умолчанию, затем введите имя глобала. После ввода пустого имени программа прекращает работу.
USER>D ^%G Device: Right margin: 80 => Screen size for padding (0=nopadding) ? 24 => For help on global specifications DO HELP^%G Global ^G ^G "" ^G(-1)=-1 ^G(-1,5)=-15 10)=-110 ^G(1)=1 ^G(1,1,1)=111 ^G(7)=7 ^G(7,"A")=7A "B")=7B Global ^
Повторы значений программа не печатает. Так что запись " 10)=-110" после записи <^G(_l,5)=-15" следует читать как "^G(_1,10)=-110".
Создадим глобал с узлами ^a(l), ^a(2), ^a(l,l) и локал b("a"), b("b"). Вставим локал b в узел ^a(1) глобала и просмотрим результат операции (листинг 3.46)
Приведённые в главе 3 сведения о языке Cache ObjectScript достаточны для создания иерархических баз данных, манипуляций данными и выполнения запросов.
В практической работе должны использоваться средства для администрирования базы данных, которые мы, к сожалению, не изучаем.
USER>S ^a="", ^a(1)="", ^a(2)="", ^a(1, 1)=""
USER>S b("a")="", b("b")=""
USER>MERGE ^a(1)=b
USER>D ^%G
For help on global specifications DO HELP^%G
Global ^a
^a
""
^a(1)=""
^a(1,1)=""
"a")=""
"b")=""
^a(2)=""
Получив общее представление о базах данных и освоив первую модель данных ("сущность —связь"), можем приступить к изучению других моделей данных. В этой главе мы бегло рассмотрим иерархическую и сетевую модели в их старых реализациях, относящихся к 70-м - 80-м годам прошлого века, и, подробно, современную иерархическую модель, используемую в СУБД Cache. В следующих главах с разной степенью подробности будут изучаться объектная, объектно-реляционная и другие модели. Подобное изобилие не должно пугать. Во-первых, они всё равно существуют, нравится ли нам это или нет. Во-вторых, мы освоим отображения моделей, что позволит "свернуть" знания, выработать единый взгляд на базы данных, независимо от используемых в них моделей данных.
Иерархии так широко распространены в моделях окружающей нас действительности, что профессионал в области баз данных не может без них обойтись. Если вы не будете работать с иерархическими моделями данных, то уж моделировать иерархии в рамках используемых моделей вам придется обязательно.
Возможно, где-нибудь вы прочли о том, что иерархическая модель была первой реализованной в базах данных моделью. Это так. Обычно за этой фразой следует утверждение о том, что иерархическая модель представляет только исторический интерес, или о том, что иерархические базы данных в настоящее время не существуют. Вот это уже неправда. Давайте разберемся.
Прежде всего, иерархии настолько распространены, что трудно представить игнорирующую их модель сколько-нибудь интересного фрагмента Мира.
Во-вторых, в информатике иерархические структуры данных встречаются на каждом шагу. Достаточно вспомнить структуру каталогов нашего компьютера или любого сайта Интернет.
В этом разделе будут бегло рассмотрены не иерархические и сетевые модели вообще, а те ограниченные их варианты, которые доминировали на рынке баз данных в 60-е и 70-е годы прошлого века. В настоящее время иерархии чаще всего связаны с XML, либо эмулируются в других моделях, например, реляционной, а сетевые модели это скорее всего семантические сети.
Зачем нужно что-то знать об устаревших моделях? По трём причинам. Во-первых, мы по возможности будем вырабатывать единый подход к любым моделям данных и строить отображения между ними. Для этого нужно знать сами модели. Точное представление иерархической, сетевой и реляционной моделей в СУБД Cache позволит нам рассматривать их отображения в главе 10. Во-вторых, для правильного понимания современного состояния дел полезно иметь какое-то представление о прошлом. В-третьих, методы доступа к данным до сих пор во многом основываются на разработках, выполненных в рамках тех старых моделей.
Помня о том, что само понятие модели данных появилось в связи с разработкой реляционной модели, перечислим основные особенности реализаций ранних моделей данных:
Конечно, перечисленные особенности относятся скорее к реализациям, чем к самим моделям. Тем не менее, классические сетевая и иерархическая модели вошли в историю именно с такими недостатками.
Существовало несколько определений сетевой модели данных. В некоторых допускались бинарные связи "многие ко многим". В варианте, предложенном CODASYL, допускаются только связи типов $$1 : n$$ и следовательно $$1:1$$. Это представление и будет рассмотрено.
Сетевая модель строится из элементов данных, типов записей и типов наборов. Как станет понятно в следующих главах, их аналогами в реляционной модели являются атрибут, отношение и связь между отношениями, соответственно.
Графически такая сеть представляется диаграммами, в которых прямоугольники изображают типы записей, а стрелки, имеющие наименования, типы набора. Для связи типа 1 : n между двумя типами записей стрелка должна исходить от типа, называемого владельцем набора, к типу, называемому членом набора (рисунок 3.1).
(рис 3.1) Тип набора
Пожалуйста, не путайте наборы значений, введённых в главе 2, и рассматриваемые здесь типы наборов, объединяющие типы записей.
Заметим, что аналоги типа набора известны и в языках программирования общего назначения. Это записи с повторениями значений элемента данных. Существуют многочленные наборы, у которых на одного владельца набора приходится более одного члена набора (рисунок 3.2), выделен класс сингулярных типов набора (рисунок 3.3), которые используются для объединения записей, не имеющих естественного владельца, либо для включения записей, которые при вводе в базу не имеют естественного владельца, но могут приобрести его впоследствии (при этом, запись перемещается из сингулярного набора в экземпляр набора с новым владельцем записи).
(рис 3.2) Многочленный набор
(рис 3.3) Сингулярный набор
Наборы реализуются программно с помощью указателей, устанавливающих связь между владельцем и членом набора. Для создания связи $$1 : 1$$ в запись-владелец отношения включается дополнительное поле, представляющее указатель на запись-член отношения. Для организации связей $$1 : n$$ и $$n : m$$ вводится дополнительный тип записей, полями которого являются указатели.
В сетевой модели тип записи может быть членом нескольких наборов. В одних он может играть роль владельца набора, а в других — роль члена набора. В этом случае вводится несколько дополнительных групп указателей, а в типе-владельце набора добавляют несколько полей, указывающих на эти группы.
В итоге множество типов записей и типов связей между ними образует сетевую структуру представляемую ориентированным графом. Вершины этого графа —типы записей, его дуги, направленные от владельца к члену набора, — связи между типами записей. Операции над данными в сетевой модели реализуются через действия со списковыми структурами.
Для того чтобы понять, какие проблемы возникают при работе со списковыми структурами рассмотрим два варианта реализации указателей: с указателем следующей записи и с массивом указателей.
Вариант с указателем следующей записи (рисунок 3.4) обеспечивает последовательный доступ к записям-членам набора. По экземпляру записи-владельца набора обеспечивается последовательный просмотр записей-членов наборов.
(рис 3.4) Набор с указателем следующей записи
Если удалить запись, то в предыдущей записи необходимо скорректировать указатель. Это недостаток. Достоинство —экономия памяти.
Вариант с массивом указателей отличается независимостью указателей, но увеличивает их число (рисунок 3.5).
(рис 3.5) Массив указателей
Из-за общности сетевой модели в ней может быть представлена любая ER-диаграмма. К недостаткам сетевой модели можно отнести сложность получаемой на её основе концептуальной схемы и трудность её восприятия пользователем.
Реализация наборов в иерархической модели, как и в сетевой, может осуществляться с помощью указателей. Иерархическая модель отличается от сетевой модели рядом особенностей. Многочленные наборы определяют отношения соподчиненности. Тип —владелец набора называется предком, подчиненный тип —потомком. Особенности иерархической модели:
Пример иерархической модели приведен на рисунках 3.6 и 3.7. Штриховыми линиями выделены виртуальные узлы и связи, которые обеспечивают физическое хранение единственного экземпляра записи, воспринимаемого на логическом уровне одновременно как экземпляр типов "Руководитель" и "Работник".
(рис 3.6) Пример древесного типа
(рис 3.7) Заполнение иерархической базы
Иерархическая база данных может представлять совокупность нескольких деревьев. В частности, некоторые из них могут описывать структуры данных, а другие — сами данные, размещённые в этих структурах. Дополнительные горизонтальные ссылки могут существовать между узлами нескольких уровней, в том числе находящихся в разных деревьях. Такие ссылки позволяют использовать один набор записей в нескольких деревьях, не дублируя его. Широкое распространение иерархической модели было обусловлено, в частности тем, что большинство организационных систем в то время имели иерархическую структуру.
Программы, реализующие операции иерархической модели, существенно проще, чем аналогичные программы для сетевой модели. При выполнении операций манипулирования данными в иерархической модели следует учитывать, что узел потомка не может существовать при удалении предка. Недостатки у иерархической модели те же, что у сетевой, плюс недостатки, связанные с ограниченностью связей.
В незапамятные, по меркам области информационных технологий, времена, в 1967 г. был создан язык MUMPS. Эта аббревиатура полного названия "Massachusetts General Hospital's Utility Multiprogramming System", была неудачна уже по тому, что слово mumps означает болезнь, именуемую в просторечии свинкой. В 1977 г. язык был стандартизирован ANSI/ISO сразу после Фортрана и Кобола и в середине 90-х переименован в язык M. Катастрофически не везло языку с именами. Не согласны — попробуйте найти сведения о нём, задав поисковику ключевое слово "M". Современную версию языка М, называемую вполне приличным именем Cache ObjectScript (сокращённо, COS), поддерживает фирма InterSystems International.
Система управления базами данных Cache (это слово в переводе с французского означает то, что приносит известность или престиж) и правда обладает многими достоинствами. Почему для изложения материала книги, которую вы в настоящий момент, как я надеюсь, читаете, выбрана именно Cache? А потому, что это единственная СУБД, в которой можно не только поработать с тремя моделями данных —иерархической, реляционной и объектной — и изучить их отображения, но и самим создавать другие модели данных, что и будет сделано в конце книги. В этой главе мы освоим основные изобразительные средства языка и, особенно, работу с древесными структурами. В первых разделах будем изучать Cache ObjectScript так, как изучают какой-нибудь QBasic в школе. Это позволит приобрести необходимые начальные навыки и не отвлекаться на мелочи при работе с деревьями.
Для нас важно, что ObjectScript — это персистентный язык. Это означает, что он работает со структурами данных, хранящимися на диске. И эти структуры представляются деревьями, может быть связанными между собой. Их мы будем изучать подробно. Позже вы узнаете, что и программа в ObjectScript—это дерево специального вида.
Не считайте, что приобретённые в процессе скитаний по деревьям знания и навыки, нужны только в Cache. В любой современной СУБД используют XML-модели, имеющие древесную структуру. Так что вы всегда и везде будете на должном уровне.
Однопользовательская версия Cache может быть инсталлирована и использована без ограничений для некоммерческих целей. Это означает, что вы можете даже создавать приложения, но не имеете права их коммерческого использования.
Заходим на сайт фирмы InterSystems (http://www.intersystems.ru) и скачиваем файл инсталляции (путь "Продукты > Cache > Скачать Cache > Скачать однопользовательскую версию Cache"). Все примеры в этой книге демонстрируются на версии установленной из файла Cache-2010.2.0.317-win_x86.exe Объём файла 292 мбайта. Существуют версии под ОС Windows, Unix, Linux, OpenVMS и Mac OS X.
Для инсталляции под Windows запустите файл инсталляции и дождитесь появления страницы с лицензионным соглашением (рисунок 3.8). Внимательно изучите его и подтвердите, что вы принимаете условия соглашения. Нажмите на кнопку "Далее".
(рис 3.8) Страница лицензионного соглашения
Выберите имя инсталляции, а лучше оставьте имя по умолчанию. Еще раз нажмите на кнопку "Далее". Появится путь, в котором устанавливается Cache. Его также можно изменить. Ещё раз нажмите на "Далее". На следующем экране (рисунок 3.9) выберите вариант по умолчанию "Разработка". Затем нажмите на кнопку "Далее".
(рис 3.9) Вид инсталляции
Остаётся выбрать кодировку—8 бит или Unicode. Выбирайте Unicode, затем параметры безопасности (ставьте на минимум). Нам они пока безразличны.
Наконец, после того, как программа выдаст итоговые сведения о варианте инсталляции, нажимаем на кнопку "Установить" и ждем завершения инсталляции.
Если Cache была правильно установлена, то внизу справа на панели задач появится изображение синего кубика. Называют его Cache-куб. Если щёлкнуть правой кнопкой по Cache-кубу, появится всплывающее меню Cache (рисунок 3.10).
(рис 3.10) Меню Cache
Первое время нас будет интересовать единственный пункт меню — "Терминал" (рисунок 3.10). Позже мы будем использовать студию и часть функций портала управления системой. Итак, выбираем пункт меню "Терминал". Окно терминала выглядит примерно так, как показано на рисунке 3.11.
(рис 3.11) Начало работы с терминалом
Подсказка USER> приглашает начать работу в пространстве имён (или в базе) USER. Всегда существуют две обязательных области имен %SYS и %CACHELIB и сколько угодно необязательных, в том числе USER. Пространство имён — это множество уникальных имён, обычно как-то связанных по смыслу. Одно имя может повторяться только в разных пространствах имён.
Область имён всегда можно сменить командой ZN:
USER>ZN "SAMPLES" SAMPLES>
Кстати, имена команд не чувствительны к регистру.
Займемся основными изобразительными средствами языка.
Вспомним, что формы Бэкуса-Наура применяются для описания синтаксиса языков программирования. Терминальным называется символ, который может быть записан в порождаемую строку текста без изменений. Нетерминальные символы разворачиваются на основе приведенных описаний.
Нетерминальные символы будем записывать в виде одного слова или нескольких слов, соединённых знаком подчёркивания, например, логиче-ское_выражение. Для каждого нетерминального символа должно существовать минимум одно определяющее его правило.
Такое правило имеет формат:
$$нетерминальный\_символ::=определение$$
Знак "::=" означает "определен как ". Используются ещё разделитель альтернатив в виде вертикальной черты "|", признак необязательного выбора [], признак обязательного выбора {} и признак повторяемости в виде многоточия ". . . ".
Символ $$\sqcup$$ в записи синтаксиса обозначает пробел. Символ $$\hookleftarrow$$ — конец строки — соответствует нажатию клавиши Enter.
Заметим, что из-за сложности разбора полного синтаксиса для начинающих в этой главе будут приводиться только неполные описания, достаточные для усвоения основ. За деталями следует обращаться к материалам фирмы InterSystems, например, выбрав позицию "Документация" (рисунок 3.10).
В одной строке можно размещать несколько команд. Определение командной строки для работы в терминале:
$$командная\_строка ::= команда[\sqcupкоманда...] \hookleftarrow$$
Командная строка состоит из одной или нескольких команд. Каждая команда состоит из командного слова, следующего за ним одного пробела-разделителя и аргументов, разделенных запятыми. Команды в одной строке разделяются не менее чем одним пробелом. За командой без аргументов должно следовать не менее двух пробелов. Количество команд в строке ограничивается лишь заданием максимальной длины строки.
Иногда можно задать несколько аргументов команды, разделенных запятыми. Если хотите, можно после запятых помещать один или несколько пробелов. Можно пробелов после запятых не делать вообще.
В число аргументов команд ввода (READ) и вывода (WRITE) можно включать символы форматирования:
Команда WRITE обеспечивает вывод информации в терминале. Допускается использование как полного имени команды WRITE, так и сокращенного до одной буквы W.
Синтаксис команды WRITE:
$$W[RITE][:постусловие]\sqcup[форматирование | аргумент\_WRITE], \dots$$,
где форматирование—один из символов !, ?, \#;
за символом ? следует число;
аргумент\ WRITE — переменная, константа или выражение.
Постусловия —это условия, при выполнении которых команда будет исполнена, иначе команда пропускается. Необходимость понятия аргумент станет понятна при изучении косвенности (раздел 3.2.4).
Заметим, что все имена команд не чувствительны к регистру.
В конструкции "?число", аргумент не должен превышать 32767. Да и зачем нам такое число пробелов? Числа меньше нуля или 0 не сдвигают маркер. Дробная часть числа просто отбрасывается и аргументом считается оставшаяся целая часть. Иначе говоря, минимальный шаг — это знакоместо.
WRITE без аргументов выводит на терминал имена и значения всех существующих на данный момент в памяти переменных, называемых локала-ми. Выполните командную строку W 0, ?20, "a", ?40, "b", !, 0 , попытавшись предсказать результат до выполнения команды на машине.
Применение перехода к новой странице — знак диез (#) — в общем случае имеет смысл, только если печатается твердая копия (в COS можно перенаправлять вывод на любое устройство). Команда W # очищает экран.
Присваивание выполняется командой SET. Её формат:
$$S[ET][:постусловие]\sqcup [присваивание][,присваивание \dots]$$
Присваивания являются аргументами команды.
Присвоим переменной x значение 1 командой $$SET\ x=1\rfloor$$ . Проверить, была ли создана переменная x, можно, набрав команду $$WRITE\ x\rfloor$$. Результатом выполнения этой команды является вывод на экран значения переменной x. Действительно оно равно 1 (листинг 3.1).
USER>SET x=1 USER>WRITE x 1 1 USER>
Вертикальную черту слева вы не увидите в терминале. В книге она обозначает что представлена картина, полученная в терминале.
Вы, конечно, заметили, что редактор терминала — строчный. Например, набрав WRITTE x, мы можем клавишей — подвести знак маркера к лишней букве "T" и удалить её клавишей Delete.
Выполнявшиеся ранее команды запоминаются. Последняя выполненная команда вызывается клавишей $$\uparrow$$. Каждое нажатие на неё вызывает предыдущую команду, пока все сохранённые записи не исчерпаются. Клавиша $$\downarrow$$ используется для обратного прохода по этим же командам.
Вспомним, что командные слова не чувствительны к регистру, так что в дальнейшем позволим себе вольности и будем писать их и заглавными и строчными буквами.
Командные слова сокращаются, как правило, до одной буквы. Так, вместо строки SET x=1 можно написать S x=1. Если же нужно создать несколько переменных, то это можно сделать следующим образом:
|USER>S z=1, Ы="", h=1.2
Для проверки значений всех существующих на текущий момент переменных можно распечатать их значения при помощи команды WRITE без параметров.
Замечание. Cache одинаково хорошо работает с обеими половинами таблицы ASCII. Так что кириллица используется без ограничений.
В дальнейшем изложении знак $$\hookleftarrow$$ проставляться не будет, так как его применение достаточно очевидно — обозначение конца командной строки и конца ввода.
Синтаксис команды READ ввода данных с терминала:
$$R[EAD][:постусловие]\sqcup[[подсказка,]аргумент] $$
В качестве аргументов используются имена переменных.
Форматирование включается в подсказку.
При исполнении команды система ждёт ввода данных Завершается ввод клавишей Enter. Система позволяет ограничить время ввода. Если набрать $$ R x:3 $$и ничего не вводить, то через 3 секунды ожидание ввода прекратится (листинг 3.2).
USER>R x:3 USER>
Точно также можно ограничить количество вводимых символов. Так, команда R x#4 позволяет ввести не более четырёх символов. Можно сочетать оба ограничения # и :. Например, R x#4:8.
Команда READ позволяет вводить значения с любого внешнего устройства, но мы этот вариант рассматривать не будем.
Примеры команды приведены в листинге 3.3.
USER>R ?10,"Введите ваше имя: ",nn Введите ваше имя: Николай USER>R ?5,"Введите ваше имя: ",nn,!,?5,"Фамилию: ",sn Введите ваше имя: Николай Фамилию: Бессарабов
Каждое введенное значение завершается нажатием на клавишу Enter. Проверьте командой WRITE введенные значения фамилии и имени.
Стирать созданные переменные можно командой KILL, сокращённо K. При этом KILL без параметров удаляет все локальные переменные. В варианте "KILL список_имён" удаляются все указанные переменные, а "KILL (список_имён)" удаляет все переменные кроме указанных (листинг 3.4).
USER>K s a=1,b=2,c=3,d=4,bc=7 7 W a=1 b=2 bc=77 c=3 d=4 USER>K a,b W bc=77 c=3 d=4 USER>K (c,d) W c=3 d=4 USER>
В первой командной строке командой K мы удалили все имевшиеся локальные переменные и создали локалы a, b, c, d, bc. Обратите внимание, что здесь после K стоит ровно два пробела. Во второй командной строке "K a, b W" удаляются переменные a и b и выводятся все оставшиеся локалы. В третьей строке "K (c, d) W" удаляются все переменные, кроме c и d, то есть удаляется bc.
В командах SET и WRITE можно вычислять сложные выражения. При этом следует помнить, что в Cache для ускорения разбора принято расшифровывать выражения слева направо, не учитывая старшинство операций. Поэтому 2 + 3 * 8 равно 40, а не 26, как учили в школе (листинг 3.5). Пользуйтесь скобками для задания порядка действий.
USER>W 2+3*8 40 USER>W "12qwerty"+4 16 USER>W "qwerty12"+4 4 USER>
Все переменные не типизированы. По сути, хранится единственный тип данных — строка переменной длины. Последовательность символов в зависимости от контекста расшифровывается слева направо, пока это возможно. Поэтому "12qwerty" в арифметическом выражении равно 12, а "qwerty12" нулю.
Выражения могут быть трех типов:
| Оператор | Выполняемое действие |
| \ | Целочисленное деление |
| ** | Степень |
| # | Остаток от деления по модулю |
| _ | Конкатенация (соединение строк) |
| ' | Логическое отрицание (NOT) |
| = | Присваивание или сравнение на равенство |
| ‘= | Сравнение на неравенство |
| [ | Содержит |
| ] | Следует за |
| ]] | Сортируется за |
| , | Логическое AND ( это "короткое" AND) |
| ||, ! | Логическое OR (|| это "короткое" OR) |
| @ | Косвенность |
| ? | Сравнение с шаблоном |
Допускаются комбинации '> не больше чем (меньше или равно) и '< не меньше чем.
Основу логических выражений образуют операторы сравнения (=, >, <, '=, [, ], ?). Заметим, что обозначение для неравенства <> не употребляется. А вот условия <= и >= можно использовать наряду с '< и '>.
Например, R x IF x <=2 W x,"<=2"
Обратите внимание, что в качестве одиночной кавычки выбирается та, которая расположена на клавише с буквой "э".
Операторы сравнения и логические константы связываются логическими операторами AND (обозначается или ), OR (обозначается | или ||), NOT (обозначается ').
Предскажите и проверьте результат выполнения трёх командных строк с постусловием:
W:"1ab" "Условие истинности" W:-1 "Условие истинности" W:0 "Условие истинности"
Ответ: Первое постусловие "^^'представляет константу, интерпретируемую как логическое значение 1. Второе условие в виде константы —1 также интерпретируется как "не 0", то есть 1. И только третье соответствует ложному значению.
Краткие варианты логического оператора "И" и || оператора "ИЛИ" отличаются от обычных и | тем, что выполнение цепочки связанных условий прекращается, как только результат определится. Для "И" достаточно получить первый ложный результат, а для "ИЛИ" первый истинный. Условия в IF, записанные через запятую, также выполняются по краткому варианту. Поэтому для повышения скорости, необходимо первыми помещать те условия, у которых больше шансов получить ложное значение для "И" и истинное значение для "ИЛИ".
Логические и арифметические выражения могут сочетаться. Логическое выражение может использоваться для выбора арифметического значения, например, $$(11*(x>7))+(27*(x<=7))$$ .
Для тренировки объясните работу следующих командных строк:
$$ S\ x=3\ W\ 11*(x>7)+27*(x<=7) $$
и
$$ S\ x=8\ W\ 11*(x>7)+27*(x<=7) $$
Мы уже обратили внимание на то, что Cache очень лояльна к именам из второй половины ASCII таблицы. В других СУБД с ними могут возникать большие проблемы. Скажем, такие имена допускаются при создании объектов, которые затем работают с ошибками. Имена переменных должны начинаться либо с буквы, либо со знака процента (%), за ними ногут следовать буквы или цифры. Системные переменные могут начинаться с символа $.
Длина имени не ограничена, но в идентификации имён участвуют только первые 31 символов. Пользовательские имена чувствительны к регистру, а предопределённые имена команд нет.
В COS нет проверки на совпадение пользовательских имён с зарезервированными именами. Можно создать переменную с именем set, например, SET set=11, но так поступать не рекомендуется. Вы можете запутать и себя и других разработчиков.
Язык COS способен создавать переменные, значения которых сохраняются на диске. В противовес временным переменным, именуемым лока-лами, хранимые переменные называются глобалами. Их имена обязательно начинаются со знака "^". Как остроумно сказано в одной книге, это галочка, которая в простонародье именуется циркумплекс. Действия над глобалами осуществляются так же, как и с обычными переменными — локалами. Но WRITE без параметра их не обнаруживает.
Даже если вы ещё не создавали глобалов, то в выбранном вами пространстве имён, например USER, они уже существуют. Для проверки войдем в портал управления системой и в разделе "Управление данными (System Explorer)" выберем "Глобалы", а затем пространство USER (рисунок 3.12).
(рис 3.12) Глобалы, созданные при инсталляции
Командой S ^а=33 в терминале создаём глобал и проверяем его наличие нажав на "Просмотр глобалов"(рисунок 3.13).
(рис 3.13) Глобал ^a создан
Выбираем "Просмотр" или "Редактировать". Теперь можно просмотреть глобал и удалить его, а в глобале с древесной структурой удалить его весь или же отдельные узлы (рисунок 3.14).
(рис 3.14) Редактирование глобала
Глобалы пространства имён XX хранятся в папке с именем XX в файле с одним и тем же именем Cache.dat. На моей машине созданный глобал^а находится в файле Cache.dat в папке C:\InterSystems\Cache\mgr\user. У вас вместо имени Cache может использоваться TryCache.
Заметим, что процессом сохранения созданных или изменённых глоба-лов пользователь управлять не может.
Предполагалось, что, выполняя предыдущие примеры, вы не делали ошибок. Вообще говорят, что люди делятся на умных, которые учатся на чужих ошибках и других, которые учатся на своих. Так вы этому не верьте. По крайней мере, в программировании это не так. Хорошо бы, конечно, никогда не делать ошибок. Но в реальности остаётся стараться не делать их слишком много, особо избегая непоправимых ошибок, и, самое главное, быстро исправлять сделанные ошибки. Научиться этому можно, только на своих ошибках.
Итак, вы решили определить переменную с неправильным именем 2x задав команду S 2x=1. Получаем сообщение об ошибке (листинг 3.6, первая командная строка). Ошибка (<SYNTAX>) означает синтаксически неправильно сформированную строку языка Cache ObjectScript.
Сделаем ещё одну ошибку, которая позволит выяснить, как исполняется командная строка (листинг 3.6, вторая командная строка). Ошибка <UNDEFINED> сигнализирует о том, что некоторая переменная не имеет значения. Имя переменной пишется после знака *.
USER>S 2x=1 S 2x=1 ^ <SYNTAX> USER>K S x=1,y=2,z=a,v=3 K S x=1,y=2,z=a,v=3 л ^ <UNDEFINED> *a USER>W x=1 У=2 USER>
Вторая ошибка произошла из-за того, что переменной z мы пытались присвоить значение неопределенной переменной а. Результат выполненной позднее в третье строке команды W показывает, что командная строка читается слева направо и те её части, которые могут быть выполнены до появления ошибки (S x=1,y=2) выполняются. Появление ошибки прекращает исполнение командной строки и присваивание S v=3 не производится, хотя эта часть команды ошибки не содержит.
Разветвления в COS могут организовываться тремя способами:
IF, отдельная команда ELSE, системная переменная $TEST, хранящая логическое значение истинности последней проверки условия);IF-ELSEIF-ELSE, переменная $TEST при работе с условиями не используется).Главное различие между вариантами IF в том, что классическая команда основана на разбиении программы на блоки, а старая команда языка М этого не предполагает, хотя и допускает.
Блоком называется последовательность команд COS, заключенная в фигурные скобки. Блок может быть помещен везде, где может помещаться единственная команда. Блоки могут быть вложены.
Любая команда может быть снабжена необязательным постусловием, записываемым через двоеточие:
$$имя\_команды[:<логическое выражение>]$$
Команда с постусловием выполняется только если логическое выражение принимает значение ИСТИННО. Вспоминаем, что в COS принята следующая трактовка числовых значений истинности: 0 это ложно, любое другое значение — истинно. В качестве примера вводите разные значения, исполняя командную строку
R z W:z=1 !,"z=1'"
В команде передачи управления GOTO постусловия могут следовать как за командным словом GOTO, так и за любым ее аргументом:
$$G[OTO][:логическое_выражение]?[аргумент][,аргумент\dots]$$,
где
$$аргумент ::= метка[:логическое\_выражение] | @простейшее\_выражение$$
Последний вариант $$@простейшее\_выражение$$ это косвенность, которой мы займёмся позже.
Команда
$$\sqcup G:x>=1 METKA1:y=1, METKA2:y>1,METKA3$$ работает, если выполнено условие $$x > 1$$, при этом переход производится к первой метке если $$y = 1$$, ко второй если $$y > 1$$ и к третьей в остальных случаях $$(y < 1)$$. Необходимость простановки пробела перед G объясняется тем, что команду имеет смысл использовать не в командной строке, а в программе. В строке программы команды записываются со второй позиции, а сами программы рассмотрим чуть позже.
Заметим, что переход из GOTO в другую программу или другую строку той же программы выполняется без возврата в следующую за вызвавшей строку. Если требуется возврат управления, используют команду D[O], в которой за командным словом также может помещаться постусловие.
Замечание. В COS метка —это любое имя. В инструментальном средстве "Студия", которую мы рассмотрим ниже, метка записывается с первой позиции строки.
Формат команды IF:
$$\sqcup I[F] <аргумент>[,<аргумент>...] <все\_команды\_до\_-конца\_строки>$$,
где
$$<аргумент>::=<логическое\_выражение>|@<проcтейшее_выражение>$$
Каждый аргумент —это условие, возвращающее логическое значение. Системная переменная $TEST хранит логическое значение последней проверки. Набор аргументов команды IF образует условие IF (пример 3.7)
|USER>S x=1 I x=1 W "x=",x, " $TEST="_$TEST
Если в IF имеется несколько аргументов, то они образуют сложное логическое условие в котором аргументы соединены логическим оператором И (AND).
Команда ELSE сделана независимой от IF. Ее формат:
$$E[LSE]\sqcup\sqcup<все\_команды\_до\_конца\_строки>$$
Часть строки, идущая вслед за ELSE выполняется, если $TEST=0. В противном случае управление передается следующей строке. Обратите внимание, что после ELSE записывается два пробела (пример 3.8)
USER>S x=1 I x=2 W "x=", x USER>W "$TEST="_$TEST USER>E W x
Существует еще безаргументный IF, записываемый в виде:
$$I[F]\sqcup\sqcup<все\_команды\_до\_конца\_строки>$$
Понятно, что он использует последнее значение $TEST, установленное другими командами. При сложных проверках, осуществляемых в ветвящихся и циклических программах, использование такого синтаксиса может сильно затруднить отладку.
Заметим, что постусловия, в отличие от команды IF, не используют переменную $TEST и потому они не могут повлиять на команду ELSE.
Все ветви разветвления передаются единственной командой:
$$\verb*|I[F] <аргумент>[,<аргумент>,...]{<блок>}|$$
$$\verb*|ELSEIF <аргумент>[,<аргумент>,...]{<блок>}|$$
$$\verb*|ELSE {<блок>}|$$
Например:
$$\verb*| R x|$$
$$\verb*| I x=2 \{W !,"x=2"}|$$
$$\verb*| ELSEIF x=3 \{W !,"x=3"}|$$
$$\verb*| ELSE \{W !,"???"}|$$
$$\verb*| Q|$$
Вы, конечно, обратили внимание, на необычную запись последнего примера и приведённой выше команды GOTO . Дело в том, что многострочная запись команды в терминале не работает и реализовать ее можно только в программе, создаваемой в инструментальном средстве Студия. А там действует правило: с первой позиции в строке записываются только метки программ, а команды занимают позиции со второй и далее. Упомянутая команда GOTO также предназначена для использования в программе.
Запустим из меню куба Cache ещё один инструмент — Студию (рисунок 3.15).
(рис 3.15) Студия
При выборе в подменю позиции "Создать" будет предложен выбор варианта. Для создания программы будем останавливаться на выборе по умолчанию "Cache ObjectScript программа". В главе 10 в студии будут создаваться классы (вариант "Класс Cache"). Многочисленные остальные возможности в этой книге не используются.
Наберем текст IF в точности так, как указано в последнем примере. Попробуйте набрать какую-нибудь команду с начала строки. На поле слева появится красная стрелка - признак ошибки в строке. Текст команды будет подчёркнут красным.
Есть два варианта компилирования программы. В первом выберите в меню последовательность "Файл>Сохранить как", затем укажите имя программы, например P1. Расширение имени пока роли не играет. Оставьте предлагаемое расширение .mac. Обратите внимание на то, что имя программы чувствительно к регистру, так что P1 и p1 это разные имена.
Во втором варианте выберите в головном меню позицию "Собрать", а затем "Компилировать" (или просто наберите комбинацию клавиш Ctrl+F7). В любом варианте выполняется компиляция.
Поскольку программа создана успешно и сохранена в виде глобала AP1.mac, ее можно вызвать из терминала с помощью команды D[O] —ВЫПОЛНИТЬ. Расширение имени указывать не обязательно. Проверьте в терминале её работу (рисунок 3.16)
(рис 3.16) Первая программа
Переменная $TEST в этой форме разветвления не задействована.
В ObjectScript, как и в большинстве языков программирования, имеются три основных типа циклов:
"FOR";"WHILE";"DO ...WHILE".Структуры команды FOR:
$$\verb*|F[OR] счётчик\_цикла=параметр программная\_строка|$$
$$\verb*|F[OR] счётчик\_цикла=параметр {блок}|$$
$$F[OR] блок$$
В однострочном варианте тело цикла образуют все команды, помещённые после команды FOR до конца строки. В простейшем случае параметр имеет одну из форм:
$$список\_выражений\_разделённых\_запятой;$$
$$начальное\_значение:инкремент;$$
$$начальное\_значение:инкремент:конечное\_значение$$
Значения счётчика цикла могут и возрастать и убывать и принимать числовые и строчные значения вперемешку.
В сокращённых версиях цикла отсутствует проверка конца. Поэтому: необходимо в теле цикла организовать выход из цикла, например,
F i=1:1 W i, ! s i=i+1 Q:i>5
Примеры:
F i=1:1:5 W i, !В Студии можно использовать циклы с блоками (листинг 3.9)
FOR {
READ !,"Number:", num
QUIT:num=""
W !,num
}
Заметьте, здесь использована простейшая безаргументная форма цикла.
Можно использовать сколько угодно параметров FOR.
Пример: F i=1:1:4, "X", 23:2:30 w !, i
При зацикливании программы наберите сочетание клавиш Ctrl/C. А теперь сами попытайтесь предсказать, как будет работать цикл: F i=1:(i<3):4 W !, i
WHILE — цикл с предусловием, DO —WHILE — цикл с постусловием. Их форматы:
$$\verb*|WHILE условие блок|$$
и
$$\verb*|DO блок WHILE условие|$$
Условие завершения в цикле WHILE выполняется до выполнения блока, а в цикле DO-WHILE после. Если значение истинно (то есть отлично от 0), то осуществляется новая итерация цикла WHILE, в противном же случае цикл завершается.
Пример цикла WHILE приведен в листинге 3.10
SET i=0
WHILE (i<10) {
SET i=i+1 WRITE i,!
}
QUIT
Результатом выполнения данной программы будет вывод на экран чисел от 1 до 10.
Выход из тела цикла возможен с помощью команды передачи управления по метке (GOTO).
В зависимости от промежуточных результатов полученных программой может меняться ход вычислений. Разветвления и циклы меняют направление прохода по алгоритму и число повторений циклов. Может потребоваться значительно больше: изменить имена, параметры или индексы, сформировав их в программе. Эти проблемы решаются использованием косвенности. Максимально гибкими оказываются программы, которые могут исполнить на ходу сформированные фрагменты текста. Для этого используется команда со странным названием XECUTE, образованным от слова execute (исполнить).
Заметим, что программы, изменяющие себя самих без всяких ограничений, конечно же, интересны, но вряд ли удобны для работы с бизнесом. Представьте, что, написав программу сегодня, назавтра вы уже не знаете, с какой именно программой работаете. Полезными могут оказаться программы, у которых меняются отдельные разделы.
Косвенность позволяет сформировать меняющиеся имена переменных и программ, аргументы, индексы и элементы шаблонов. Различают четыре варианта:
Последним вариантом —косвенным шаблоном —мы заниматься не будем из-за того, что шаблоны в ObjectScript плохо приспособлены к работе с кириллицей.
Рассмотрим косвенное имя. Косвенность в имени обозначается символом @, проставляемым перед любым простым выражением. Простое выражение — это любое выражение языка, не содержащее бинарных операторов. Можно использовать и выражение с бинарными операторами, заключенное в скобки.
Начнем с простого примера (листинг 3.11, первая командная строка).
(1) USER>S a="b",b=77, w a,!, @a
b
77
(2) |USER>S a1=1, a2=2, a3=3 F i=1:1:3 W @("a"_i)
123
(3) |USER>S a1=1,a2=2,a3=3, ab="ab" F i=1:1:3,"b" W @("a"_i)
123ab
Здесь мы задали переменной a значение "b", переменной b присвоили значение 77 и распечатали командой WRITE сначала значение а, затем @a. Поскольку использована косвенность, печаталось значение той переменной, имя которой записано в а, то есть 77.
Следующие два примера (листинг 3.11, вторая и третья командные строки) показывают формирование имени переменной в цикле с помощью косвенности. Имена переменных создаются в цикле с помощью конкатенации. Это позволяет в цикле читать набор переменных a1, a2, a3 не образующих массива. Скобки в операторе W обязательны. Иначе косвенность распространялась бы только на переменную a, тогда как необходимо использовать выражение "a"_1.
Косвенность вкладывается в косвенность на любую глубину, например, S a="b", b="c", c=9 w a, !, @a, !, @@a
Предскажите, а затем проверьте результат выполнения следующей строки:
S a1=1, a2=2, a3=3 F i=1:1:3 W @"a"_i
Для того, чтобы завершить тему косвенного имени рассмотрим пару примеров, которые рекомендуется выполнить после изучения программ в ObjectScript (раздел 3.2.8). Пусть имеется любая программа PRG.INT с меткой MET1. Косвенность применима к именам программ и к меткам:
USER>S rout="prg" D A@rout USER>S a="MET1", b="PRG" D @aAPRG, MET1A@b, @aA@b
В последнем примере три раза подряд вызывается PRG с метки MET1.
Вызов программы с косвенным именем может снабжаться постусловием: |USER>D A@rout:x=1
Порядок действий при использовании косвенности следующий: сначала раскрывается косвенность, затем выясняется существование программы и только потом проверяется условие. Предположим, что вызываемая программа не существует. Если бы сначала проверялось условие, то при его ложности, так и не удалось бы установить, что программа не существует.
Косвенный аргумент позволяет программе задать как один аргумент, так и список аргументов (листинг 3.12, первые четыре командных строки).
USER>S b=77, a="b=2"
USER>S @a W b 2
USER>K b,c S x="b=2,c=3",@x W b," ",c
2 3
USER>S b=2,x="b=2" I @x W x
b=2
USER>S b=2,x="b=2" W:@x x
S b=2,x="b=2" W:@x x
^
<SYNTAX>
В первой командной строке переменной b присвоили начальное значение 77. Значением a во второй строке делаем текст "b=2". Косвенный аргумент в команде второй строки "S @a" раскрывает эту команду как "S b=2", и b присваивается значение 2.
А вот попытка переписать эту строку с постусловием во WRITE не удастся (листинг 3.12, последняя командная строка). Дело в том, что условие, помещаемое после двоеточия это не имя, и не аргумент. Косвенность здесь не применима.
Аргумент цикла FOR не может быть косвенным, то есть нельзя писать, например,
S x="i=1:1:3" F @x W "Z",
но счетчик цикла FOR может использовать косвенность:
S u="i" F @u=1:1:3 W @u
В цикле WHILE ситуация аналогична, то есть единственный вариант использования косвенности это что-то вроде: WHILE @u<3 {S i=i+1 W i}
В сложных случаях форму косвенности определить не легко, хотя общее правило просто и понятно: имеется та форма косвенности, после раскрытия которой получается синтаксически правильное выражение.
Дерево в COS представляется многомерными разреженными масссива-ми. Программа для работы с деревьями должна обеспечить доступ к узлам на любом уровне. Индексная косвенность как раз позволяет формировать индексы во время работы программы. Запись имени узла с использованием индексной косвенности:
$$@имя\_узла@(список\_индексов)$$
Как видите, особенность синтаксиса индексной косвенности в том, что знак @ повторяется дважды.
Создадим локал, имеющий древесную структуру (листинг 3.13) и используя индексную косвенность сформируем команду W, извлекающую значение узла a(1,1,1) .
USER>S a(1)=1,a(2)=2,a(1,1)=11 USER>S a(1,1,1)=111,a(1,1,2)=112 USER>S x="a(1)" w "a(1,1,1)="_@x@(1,1) a(1,1,1)=111
Обратите внимание на то, что индексная косвенность может использоваться только в приведенной выше форме. Иначе говоря, нельзя опускать один из знаков @ и потому, например, команда "S a(1,1)@(2)=77" вызовет сообщение об ошибке.
Команда XECUTE (сокращённо X) позволяет исполнить однострочную подпрограмму, хранимую как значение некоторой переменной:
S z="W 22,", v="!,1+7" X z_v
В результате будет исполнена сформированная командная строка "W22,!,1+7 ".
Заметим, что исполнение команды может передать управление куда угодно, но адрес возврата из XECUTE остаётся прежний —на командную строку, следующую за XECUTE. Для проверки создадим подпрограмму с именем AA следующего содержания:
W "В подпрограмме " Q
Исполним команду XECUTE, вызывающую передачу управления:
USER>X "G AA W 77" W "После подпрограммы",! Q В подпрограмме После подпрограммы
Команда WRITE 77 не была исполнена, что и следовало ожидать.
Мы уже встречались со строковыми константами, представляющими последовательность символов, заключенных в двойные кавычки. Набор используемых символов зависит от сделанного при установке Cache выбора кодировки (Unicode или UTF8). Длина строки не превышает 32 К символов.
Поскольку двойная кавычка ограничивает строковую константу возникает вопрос, что делать, если символ " встречается внутри текста? В этом случае её просто удваивают:
USER>W "a""b" a"b
Длина пустой строки —ноль. В команде READ вводимой переменной присваивается пустая строка каждый раз, когда ввод состоит из единственного нажатия клавиши Enter.
В стандартном ASCII-терминале длина строки 80 символов. При выводе длинная строка разрезается. При вводе необходимо продолжать набор символов, не обращая внимание на самопроизвольный переход маркера на следующую строку (рисунок 3.17).
(рис 3.17) Очень длинная строка
При выборе UTF8 допускается использование символов кириллицы и псевдографики, занимающих вторую половину кодовой таблицы ASCI ОТ 128 до 255).
Над строками определены пять бинарных операций:
Конкатенация соединяет оба операнда в одну строку, записывая операнды последовательно слева направо (листинг 3.14).
USER>W "conca"_"te"_"nation" concatenation
Рассмотрим операторы строкового сравнения =, [ , ] и ]] . Оператор = проверяет строки на идентичность.
Оператор "содержит" обозначается символом "[ " и возвращает логическое значение. Запись s[t означает проверку того, что строка t входит в строку s хотя бы один раз (листинг 3.15):
USER>S x= "abcd"["bc" W x 1 USER>S x= "abcd"["aa" W x 0
Операторы следования и сортировки могут показаться мелочно сложными, не интересными, а может быть и не нужными. Тем не менее, рассмотрите их внимательно и во всех деталях, чтобы понять как Cache упорядочивает узлы дерева одного уровня.
Оператор "следует за" обозначается как закрывающая прямая скобка "] ". Строка s следует за t, то есть выражение t]s истинно, если выполняется одно из трех условий:
строка t непустая, а s пустая, то есть пустая строка следует за любой непустой (листинг 3.16):
USER>S x="abc"]"" W x 1
строки s и t непустые и код ASCII первого символа t превышает код ASCII первого символа строки s (листинг 3.17):
USER>S x="cd"]"axz" W x 1 USER>S x="cd"]"exz" W x 0
начала строк совпадают, но для оставшихся частей выполняется одно из двух предыдущих условий (листинг 3.18):
USER>S x="aacd"]"aabxz" W x 1 USER>S x="aacd"]"aa" W x 1
Оператор "сортируется за", обозначается ]] . В предыдущем операторе "следует за" оказывается, что вопреки здравому смыслу, но в соответствии с правилами лексикографического упорядочения число 10 предшествует 9 потому, что код первого символа "1" меньше кода "9". Так командная строка S x="10"]"9"W x отрицает, что 10 следует за 9. Оператор ]] устраняет этот недостаток. Проверим:
USER>S x="10"]]"9" W x 1
В самом деле, число 10 больше 9, то есть следует за 9.
Поскольку все типы данных, за исключением больших, записываются строками, необходимо интерпретировать строку в соответствие с ее типом. Важно помнить правило числовой интерпретации строки: "Строка в арифметическом выражении читается слева направо и понимается как число до тех пор, пока встречаются символы цифр (0,. . . ,9), буквы "E" или "e" между двумя цифрами, понимаемые как разделитель мантиссы и порядка в экспоненциальной форме записи числа". На первом символе не входящем в указанный набор числовая интерпретация прекращается. Если строку не удается интерпретировать как число, ей присваивается значение ноль (листинг 3.19).
USER>S x="5 apples"+"7 books" W x 12 USER>S x=+"abc" W x 0 USER>S x=-".7e2cccw" W x -70 USER>S x=".7e2cccw" W x .7e2cccw
Работая со строками, нужно уметь определить длину строки (это делает функция $LENGTH), выделить фрагмент строки (функция $EXTRACT), проверить вхождение подстроки в строку (функция $FIND или оператор "[" ). Может потребоваться замена одних символов другими (функция $TRANSLATE).
Функция $L[ENGTH] возвращает длину строки символов. При работе с простыми строками без разделителей используется одноаргументая форма функции (листинг 3.20):
USER>W $L("12345678")
8
USER>W $L("")
0
Функция $E[XTRACT] выделяет из указанной строки подстроку. Второй аргумент указывает начальную позицию, с которой вырезается подстрока, а третий аргумент — конечную позицию (листинг 3.21):
USER>W $E("12345678",3,5)
345
USER>W $E("Those evening bell",7,13)
evening
Второй и третий аргументы, если они имеются, интерпретируются как целые числа.
USER>W $E("Those evening bell",7.9,12.9)
evenin
Если значение второго аргумента, определяющее первую позицию меньше нуля, то выделение начинается с первой позиции строки:
Если третий аргумент превышает длину исходной строки, строка выделяется до конца. Этим пользуются для задания условия "до конца строки". Достаточно в третьем аргументе указать число, превышающее максимально допустимую длину строки.
USER>W $E("Those evening bell",15,9999)}
bell
Если позиция начала подстроки больше позиции ее конца, скажем $E("12345678",10,5), то функция возвращает пустую строку. Пустая строка выделяется и тогда, когда начальная позиция подстроки больше длины исходной строки.
Функция $F[IND] определяет, встречается ли строка заданная ее вторым аргументом в строке заданной первым аргументом. Если подстрока не найдена, то возвращается ноль:
USER>W $F("12345678","x")
0
Если подстрока найдена, возвращается номер позиции следующей за концом найденной подстроки:
USER>W $F("12345678","45")
6
Полезно понять, зачем выдаётся именно эта позиция.
В трехаргументной форме функции $FIND третий аргумент задает номер символа исходной строки, с которого начинается поиск. Поскольку время поиска, пропорционально числу проанализированных символов, задание третьего аргумента может существенно уменьшить время поиска:
USER>W $F("12345678","8",7)
9
Если значение третьего аргумента превышает общую длину исходной строки и второй аргумент не пустая строка, то возвращается ноль.
В соответствии с классической теорией множеств в Cache ObjectScript пустая строка содержится в любой строке, в том числе и в пустой:
USER>W $F("123","")
1
USER>W $F("","")
1
Для поиска всех вхождений подстроки в строку необходимо использовать циклы (листинг 3.22).
USER>S p=1 F {S p=$F("1111", "1",p) Q:p=0 W !,p}
2
3
4
5
Функция $TR[ANSLATE] используется с двумя аргументами
$$$TR[ANSLATE](выражение1, выражение2)$$
или тремя
$$$TR[ANSLATE]( выражение!, выражение2, выражение3)$$
В двухаргументной форме из строки, определённой первым выражением удалятся все символы, имеющиеся в строке, определённой вторым выражением. Например:
USER>W $TR("стpокa","оa")
"стрк"
В трёхаргументной форме строки s2 и s3, определённые вторым и третьим выражениями, определяют таблицу трансляции. Символу в n-ой позиции строки s2 соответствует символ в n-ой позиции строки s3. Если длина s2 больше длины s3, то символы s2, у которых нет соответствия, будут удалены:
USER>W $TR(QUOT;3TO e-mail", "maile-", "мыло") Это мыло
Элементы строк с разделителями называют полями.
Символ-разделитель нельзя употреблять в самих полях. Одной строкой можно записать, например, список фамилий работников отдела "Ива-нов^Петров^Сидоров". В качестве разделителя можно использовать любой печатаемый символ, в том числе и пробел. Естественно, в этом случае поле не должно содержать пробел. Поля также могут быть строками с разделителями, но разделитель внутри поля должен быть другим. Можно и эти поля представить в виде переменных с разделителями и т.д.
Похоже, что многоуровневое вложение полей не очень удобно. Тем не менее, строки с разделителями могут существенно расширить возможности базы данных. В качестве мысленного упражнения полезно представить этот раздел без рисунков в виде многоуровневой строки с разделителями. Какие разделители выберете?
Очевидно, для строк с разделителями должны поддерживаться следующие операции:
В Cache ObjectScript для решения этих задач используются функции $LENGTH, $FIND, $EXTRACT, $PIECE.
Итак, выбираем в качестве разделителя печатаемый символ, который не может использоваться в тексте полей, скажем: """, "+" и т. д. Строка с разделителями создаётся обычным присваиванием (команда "SET"). Создадим две строки str1="A~77~BD" и str2="A+B~A/B~A B" (листинг 3.23),
определим их длины (не в символах, а в полях) и выделим поля со второго по третье в строке str1.
USER>S str1="A^77^BD" USER>S str2="A+B^A/B^A B" USER>W "Длина str1=",$L(str1,"^") Длина str1=3 USER>W "Длина str2=",$L(str2,"^") Длина str2=3 USER>W $P(str1,"^",2,3) 77^BD
Количество полей определяется уже известной функцией $L[ENGTH] , в которой во втором аргументе записывается знак разделителя. Выделяется нужное поле функцией $PIECE, имеющей формат:
$$$P[IECE](строка, разделитель, первое_поле,последнее\_поле)$$
Например, функция $P(str,"^",1) выделяет первое поле, а $P(str, "^",1,3) — поля с первого по третье. Функция $PIECE относится к так называемым левым функциям. Это означает, что она может стоять в левой части команды присваивания. Вставим текст "QQ" на место второго поля, добавим четвёртое поле и седьмое поля (листинг 3.24).
USER>S str1="A^77^BD" USER>S $P(str1,"^",2)="QQ" w str1 A^QQ^BD USER>s $P(str1,"^",4)="PP" w str1 A^QQ^BD^PP USER>s $P(str1,"^",7)="PP" w str1 A^QQ^BD^PP^^^PP
Заметим, что вставка в седьмое поле привела к созданию пустых пятого и шестого полей "A^QQ^BD^PP^^^PP", что и вызвало появление в записи строки двух дополнительных циркумплексов.
Одна из основных структур данных — список — создаётся специальной функцией $LISTBUILD (сокращенно $LB). Её формат:
$$$LISTBUILD(элeмeнт\_спискa [,элемент_списка ...])$$
Создадим список из двух элементов и с помощью команды ZZDUMP разберёмся с его структурой (листинг 3.25).
USER>S x=$LB("Red","Blue")
USER>ZZDUMP x
0000: 05 01 52 65 64 06 01 42 6C 75 65
В левой части строки коды символов, а в правой отпечатанная строка. Точками обозначены непечатаемые символы. Оказывается, элементы списка разделяются парами непечатаемых символов. Первый символ такой пары — это число в шестнадцатиричной системе счисления, равное количеству символов в следующем за разделителем элементе списка плюс 2. Вторым идёт символ с кодом 01. Разделители в конце списка не ставятся. Зададим список из одного пустого элемента и список из единственного элемента, представляющего пустую строку (листинг 3.26). Пустой список эквивалентен пустой строке
USER>S y=$LB(), z=$LB("") ZZDUMP x,y
0000: 01
0000: 02 01
При вставке в список значения не определённой переменной ошибка не появляется, а вставляется NULL (листинг 3.27).
USER>K a S list1=$LB(1,a,3) USER>F i=1:1:3 W "i=",i,?10,$LISTGET(list1,i),! i=1 1 i=2 i=3 3
Двухаргументная функция $LISTGET была использована для извлечения из списка указанного первым аргументом элемента с номером, указанным вторым аргументом.
Результат конкатенации двух списков есть список. Для работы со списками кроме их создания необходимо определять правильность списков (функция $LISTVALID) и их длину (функция $LISTLENGTH), сравнивать списки ($LISTSAME). Навигация по спискам производится с использованием функций $LISTDATA, $LISTFIND, $LISTGET, $LISTNEXT. Предусмотрены преобразования строк с разделителями в списки (функция $LISTFROMSTRING) и обратное преобразование ($LISTTOSTRING).
Булева функция $LISTVALID проверяет правильность списка. Для правильных списков возвращает 1, а для неправильных 0. Формат:
$$\$LISTVALID(вырaжeниe)$$
или
$$\$LV(вырaжeниe)$$
Проверяемый список должен быть создан с помощью функций $LIST-BUILD и $LISTFROMSTRING, либо извлечён из существующего списка с помощью $LIST. В листинге 3.28 приведено несколько примеров, достаточных, чтобы разобраться с деталями.
USER>S r="A",s=33,t="",u=$LB("A","B"),v=$LB(1)
USER>W $LV(r),?10,$LV(s),?20,$LV(t)
0 0 1
USER>W $LV(u),?40,$LV(v)
1 1
USER>S y=$LB(NULL) w $LV(y)
1
Вы видите, что пустой элемент t есть правильный список.
Возвращает длину списка. Неопределённые элементы засчитываются. Вложенные списки учитываются как один элемент.
$$\$LISTLENGTH(список)$$
или
$$\$LL(список1)$$
Проверяет совпадение списков. Формат:
$$\$LISTSAME(список1, список2)$$
или
$$\$LS(список1, список2)$$
Примеры употребления приведены в листинге 3.29.
USER>s x=$LB("A","B"), y=$LB("B","A"), z=$LB("A","B"), v=$LB(), w=$LB("")
USER>w "$LS(x,y)="_$LS(x,y),!,"$LS(x,z)="_$LS(x,z),!,"$LS(v,w)="_$LS(v,w),!
$LS(x,y)=0
$LS(x,z)=1
$LS(v,w)=0
Обратите внимание на то, что пустая строка (в примере в листинге 3.28) и список из одного пустого элемента (x) оба допустимые списки, но они не равны между собой.
Формат:
$$\$LISTDATA(список, позиция)$$
или
$$\$LD(список, позиция)$$
Функция $LISTDATA проверяет указанный элемент списка и возвращает единицу, если элемент в такой позиции существует и имеет значение. Если нет такой позиции или значение элемента не определено, то вернётся ноль (листинг 3.30)
USER>k s x=$LB("Один",,y,"","Пять")
USER>s pos="Позиция "
USER>f i=0:1:6 w pos,i,?15,"$LD(x,",i,")=",$LD(x,i),!
Позиция 0 $LD(x,0)=0
Позиция 1 $LD(x,1)=1
Позиция 2 $LD(x,2)=0
Позиция 3 $LD(x,3)=0
Позиция 4 $LD(x,4)=1
Позиция 5 $LD(x,5)=1
Позиция 6 $LD(x,6)=0
В $LISTDATA, как в остальных функциях для работы со списками, "список" —это выражение, дающее имя существующего списка. Если такого списка нет, возникает ошибка UNDEFINED.
Функция $LISTFIND это наша старая знакомая $FIND приспособленная для работы со списками. Она ищет первое вхождение указанного значения в качестве элемента списка. Совпадение должно быть точным. Формат:
$$\$LISTFIND(список, значение, начальная\_позиция)$$
или
$$\$LISTFIND(список, значение)$$
Если элемент найден, возвращается его номер, а если не найден, вернётся 0 (листинг 3.31).
USER>S x=$LB("A","BB","B","C")
USER>W "$LF(x,""A"")=",$LF(x,"A")
$LF(x,"A")=1
USER>W "$LF(x,""B"")=",$LF(x,"B")
$LF(x,"B")=3
USER>W "$LF(x,""BB"")=",$LF(x,"BB")
$LF(x,"BB")=2
USER>W "$LF(x,""D"")=",$LF(x,"D")
$LF(x,"D")=0
Прикрепление пустого элемента, как к голове, так и к хвосту списка, не изменяет список (листинг 3.32).
USER>ZZDUMP x 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C USER>ZZDUMP ""_x 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C USER>ZZDUMP x_"" 0000: 03 01 41 04 01 42 42 03 01 42 03 01 43 ..A..BB..B..C
Необходимо помнить, что сравнение выполняется после приведения данных к так называемой канонической форме. Для списка созданного командой
S y=$LB("1.0","+2","003","2*2")
все следующие функции возвращают 0 потому, что числа не приводятся к текстовым константам: $LF(y,1.0), $LF(y,+2), $LF(y,003), $LF(y,4). А для списка созданного командой S y=$LB(7,6) обе следующие функции возвращают 1 потому, что вторые аргументы этих функций согласуются с элементами списка: $LF(y,7.000), $LF(y, 006).
Возвращает элемент списка в указанной позиции или значение по умолчанию, если этот элемент не определён. Формат:
$$\$LISTGET(список, позиция [, значение\_по\_умолчанию])$$
или
$$\$LG(список, позиция [, значение\_по\_умолчанию])$$
В листинге 3.33 показан пример использования функции $LISTGET и сравнение её с функцией $LIST, которая выдаёт ошибку при появлении неопределённого элемента.
USER>S y=$LB("A",,"B")
USER>F i=1:1:3 W "Элемент ",i,?12,$LG(y,i,"null"),!
Элемент 1 A
Элемент 2 null
Элемент 3 B
USER>F i=1:1:3 W "Элемент ",i,?12,$LI(y,i),!
Элемент 1 A Элемент 2
F i=1:1:3 W "Элемент ",i,?12,$LI(y,i),!
^
<NULL VALUE>
USER>
Извлекает элементы списка последовательно. Работает очень быстро. Формат:
$$\$LISTNEXT(список, указатель, значение)$$
Локальная переменная "указатель" перед первым вызовом функции должна быть установлена в 0. Тогда список будет анализироваться с начала.
Локальная переменная "значение" не может быть массивом. Инициализировать её не следует. Функция будет помещать в неё значения элементов списка (листинг 3.34).
USER>S y=$LB("A","B","C"), p=0
USER>F i=1:1:10 W "i=",i,?6,"Ф=",$LISTNEXT(y,p,v),?10,"p=",p,?20,"v=",v,!
i=1 Ф=1 p=589827 v=A
i=2 Ф=1 p=589830 v=B
i=3 Ф=1 p=589833 v=C
i=4 Ф=0 p=0 v=C
i=5 Ф=1 p=589827 v=A
i=6 Ф=1 p=589830 v=B
i=7 Ф=1 p=589833 v=C
i=8 Ф=0 p=0 v=C
i=9 Ф=1 p=589827 v=A
i=10 Ф=1 p=589830 v=B
Когда $LISTNEXT достигнет конца списка, она вернёт 0, переустановит указатель в 0 и оставит предыдущее извлечённое "значение". Продолжится анализ списка с начала. Элементы вложенных списков не выделяются. Удобно использовать функцию с циклом WHILE. Проверьте командную строку: S p=0 WHILE $LISTNEXT(y,p,v) {W !,v}.
Создаёт список из строки с разделителями. Формат:
$$\$LISTFROMSTRING(строкa, ограничитель) $$
или
$$\$LFS(строкa, ограничитель)$$
Пример приведен в листинге 3.35.
USER>S string="A^B^C", list=$LFS(string,"^") USER>ZZDUMP list,string 0000: 03 01 41 03 01 42 03 01 43 ..A..B..C 0000: 41 5E 42 5E 43 A^B^C USER>
Преобразует список в строку с разделителями. Если разделитель не указан, выбирается запятая. Отсутствие флага или нулевое его значение определяет появление ошибки <NULL VALUE>. Формат:
$$\$LISTTOSTRING(список[,огрaничитeль][,флaг])$$
или
$$\$LTS(список[,огрaничитeль][,флaг])$$
Обратите внимание на то, что функция $LTS не проверяет наличие в исходном списке разделителя, который будет использован в преобразовании. Поэтому в последнем примере в листинге 3.36 ошибочно получена строка из трех элементов с разделителем "пробел".
USER>S x=$LB("A","B"), y=$LTS(x,"разделитель")
USER>ZZDUMP y
0000: 0041 0440 0430 0437 0434 0435 043B 0438 Aраздели
0008: 0442 0435 043B 044C 0042 тельB
USER>S z=$LB("A B","C")
USER>S y=$LTS(z," ")
USER>ZZDUMP y
0000: 41 20 42 20 43 A B C
USER>
Программа (routine) — это, как всегда, именованный блок кода, предназначенный для выполнения некоторого набора операций. Имя программы не должно содержать знаков подчеркивания ("_"), тире ("-") и точки с запятой (";").
Используются три расширения имени .mac, .inc и .int. Исходный текст программы, написанной на языке Cache Basic имеет расширение .bas.
Файл исходного кода с расширением .mac обрабатывается макропрепроцессором, который транслирует макросы и встроенные SQL и HTML, получая файл с расширением .int. А этот файл транслируется в исполняемый объектный код с расширением obj. Тексты HTML используются в серверных страницах, которые в этой книге не рассматриваются.
Создавать программу можно в любом текстовом редакторе, но лучше воспользоваться Студией, с которой мы уже познакомились.
Кроме определённых в разделе 3.2.2 простейших командных строк, состоящих из команд записанных через пробел, в программах используются другие типы командных строк:
По поводу меток необходимо сделать несколько разъяснений. Во-первых, метки могут быть общедоступными (public) и внутренними (private). По умолчанию устанавливается значение public. Такая метка может быть вызвана из любой программы, а метка private — только из текущей программы.
Во-вторых, метки можно снабдить списком формальных параметров, заключённым в круглые скобки. Подпрограмма, которая будет вызвана с такой метки, воспримет переданные параметры.
В зависимости от того, возвращает ли программа значение, вызывать её из другой программы или терминала можно двумя способами, командой D[O] или помещением в правую часть присваивания (рисунок 3.18)
(рис 3.18) Два способа вызова программы-функции
Программа может содержать одну или несколько меток. Одна из них может помещаться в самом начале программы перед первой командой. Уже упоминалось, что имя метки пишется в Студии с первой позиции, в отличие от команд, перед которыми помещается минимум один пробел. Простейшая программа приведена на рисунке 3.16..
Тексты двух программ с метками и с вызовом программы приведены на рисунке 3.19. В программе MyProc1 в первой строке указано, что метка снабжена параметрами MyProc1(x,y), а переменная z заключена в квадратные скобки, указывающие, что она глобальная. В терминале видно, что программа MyProc1 просто создала локал z, а переменные x и y не доступны вне MyProc1.
(рис 3.19) Программы и метка public
Существует некоторая "нелогичность" в записи вызова по метке с параметрами. Следует писать фактические параметры после имени программы:
$$имя\_метки\ \hat{}\ имя\_программы(фактические\_параметры)$$
По умолчанию сама программа определяется как public. Вариант private смысла не имеет. Программа может содержать в себе подпрограммы, процедуры и функции.
Процедура это именованный блок кода программы, имеющий формат:
$$имя\_процедуры(формальные\_параметры)\verb*| |[общедоступные\_переменные] \verb*| |доступ\verb*| |{код\_тела\_процедуры}$$
Доступ определяет, можно ли вызывать процедуру из других программ. Это либо слово private, либо необязательное ключевое слово public (общедоступная).
Процедура начинается с метки-имени процедуры, за которым могут следовать список формальных параметров и список общедоступных переменных.
Все метки внутри процедуры имеют тип private и могут использоваться только внутри процедуры. Указание слова public вызовет ошибку.
Команда GOTO внутри процедуры может ссылаться только на внутренние её метки. Перед закрывающей фигурной скобкой выполняется неявная команда QUIT. Косвенность и команда XECUTE выполняются как внешняя по отношению к процедуре программа. Все переменные процедуры, за исключением описанных в списке общедоступных переменных, внутренние. Пример процедуры уже рассмотрен нами (рисунке 3.19).
Функции бывают внутренние и внешние. Внутренняя функция —это размещённая внутри программы процедура, возвращающая значение. Для этого в команде завершения после QUIT записывают выражение, значение которого возвращается, например, Q x+y, как в программе на рисунке 3.18.
Итак, функция отличается от подпрограммы только способом завершения. Для этого в теле функции помещается команда
$$quit\ выражение$$
Для вызова внутренней функции достаточно указать её имя. При вызове внешней функции перед именем помещается два знака доллара.
Пример: процедуру, описанную следующим образом в программе:
^demofunc sum(a,b) W "Вычисляем сумму "_а_"и"_b,! Q a+b
можно вызвать и как процедуру
USER>DO sum^demofunc(1,2) Вычисляем сумму 1 и 2
и как функцию — с возвратом значения
USER>WRITE $$sum^demofunc(1,2) Вычисляем сумму 1 и 2 3
Вызовы по ссылке и по значению понимаются в обычном смысле. При вызове по значению для формальных параметров в начале исполнения вызываемой программы выполняется неявная команда NEW, а по завершении её неявная команда KILL. Поэтому формальные параметры существуют только в вызываемой программе.
При вызове по ссылке вызываемая программа получает указатель на переменную вызывающей программы. Поэтому любое действие в вызываемой программе — присваивание и даже удаление командой KILL — влияет на эти переменные.
Имя переменной вызываемой по ссылке отмечается точкой перед именем.
Два важных замечания:
(рис 3.20) Вызов по ссылке
Заметим, что в последнем примере при вызове по ссылке с неопределённым фактическим параметром .var после выхода из подпрограммы образовался локал с древесной структурой var, var(1), var(2), var(1,1), копирующий структуру локала x заданного в подпрограмме.
Вспомним, что в ObjectScript используется четыре типа файлов программ с расширениями имён .mac, .inc, .int и .obj. Исходный текст программы (макрокод) помещается в файл с расширением .mac.
Файлы .inc содержат программы, включаемые в основную программу .mac с помощью команды include, что определило расширение имени. Основное назначение — построение макробиблиотек.
Файл .mac компилируется в файл промежуточного кода с расширением .int, а затем в исполняемый код, помещаемый в файл .obj. Для исполнения программы используется только объектный код.
Формат команды include:
$$\verb*|#Include имя|\_файла$$
Исполняемая программа загружается в пространство адресной памяти, называемое разделом. Промежуточный код, представляемый как файл с расширением .int хранится в глобале ^ROUTINE, находящемся в том же пространстве имён, что и сама программа. Структура глобала:
^ROUTINE("имя программы",0,0) = количество строк
^ROUTINE("имя программы",0,1) = первая строка
^ROUTINE("имя программы",0,2) = вторая строка и
т.д.
Остальные типы программ тоже размещаются в глобалах: .mac в ^rMAC, .inc в ^rINC, .obj в ^rOBJ. Структура файла ^rMAC такая же, как у ^ROUTINE . При вызове программы по имени без расширения сначала ищется файл .mac, а затем .int. Хранятся старые версии файлов .mac и .inc. Предпоследние версии имеют расширения .mac.1 и .int.1. У более старых версий расширения .mac.2 и .int.2.
Если, например, командой SET изменить узлы ^ROUTINE, то объектный код не будет совпадать с исходным текстом. Однако, Cache не пытается синхронизировать версии .int, .mac, и .obj. Только после новой компиляции они будут соответствовать друг другу.
Для чтения исходных текстов будем использовать функцию $T[EXT]. Её синтаксис:
$$\$TEXT(мeтка+смeщeниe\ \hat{}\ программа)$$
Можно использовать косвенность $TEXT(@выражeниe). Функция возвращает строку исходного текста программы, хранящуюся в указанном месте.
Разберитесь с текстами программ (рисунок 3.21), одна из которых распечатывает свой текст (SelfOutput.mac), а вторая (Text.mac) может распечатать текст любой программы.
(рис 3.21) Программы, распечатывающие программы
В ObjectScript программа может изменять себя и другие программы. Основные команды, участвующие в этом процессе, удобно рассматривать на основе имеющейся несколько архаичной возможности создания и изменения программы непосредственно из терминала. Введём в терминале несколько командных строк, начиная их знаком табуляции:
USER><TAB> командная_строка … USER><TAB> командная_строка
Затем без символа табуляции наберём команду сохранения и компиляции программы
$$USER>ZSAVE \verb*| |имя\_программы$$
Ниже приведен пример ввода программы в терминале и её исполнения.
USER> W "Исполнена первая строка",! USER> W "Исполнена вторая строка",! USER>ZSAVE test USER>D ^test Исполнена первая строка Исполнена вторая строка USER>S y="ZINSERT "" W 77"":+2" USER>X y USER>D ^test Исполнена первая строка Исполнена вторая строка 77
Вставка в программу в качестве третьей строки команды W 77 выполнена с использованием XECUTE для предварительно сформированной строки, в которой определён аргумент в виде команды, которая будет вставлена "w 77" и задано смещение от начала программы +2. Это ZINSERT "W 77":+2.
Команда
$$\verb*|ZR[EMOVE] смещение|$$
удаляет строку.
С помощью Студии проверьте существование этой программы. Учтите, что она создаётся с расширением .int.
Для работы с текстами программ используют ещё две команды:
Удаление программы из раздела $$\verb*|ZR[EMOVE] имя|$$
загрузка программы в раздел $$\verb*|ZLOAD имя|$$
Замечание. Для практических целей следует использовать программу ^%r и класс %Library.Routine, которые работают гораздо быстрее.
В COS имеется уникальная возможность измерения промежутков времени с разрешением по-видимому до нескольких микросекунд. С этой целью используется функция $ZH, которая фиксирует временную метку:
USER> w $zh 3819.544684
Первая часть числа 3819 —это число секунд от включения машины до времени исполнения $ZH, а вторая часть это дробные доли секунды.
Если отметить время до и после выполнения команды, командной строки или другой конструкции, то разность временных меток даёт длительность процесса.
Значительные разбросы результатов получаются из-за того, что невозможно остановить процессы операционной системы, не связанные с исполнением нашей программы. Поэтому для получения достоверных результатов необходимо многократно повторять измерения и усреднять оценки.
Исполняя программу ^zhtime.mac (листинг 3.37) при одном и том же числе повторений и при разном их числе можно прийти к выводу о том, что единичное исполнение функции $ZH, даёт большие разбросы, а при числе повторений более 105 получаются стабильные результаты (таблица 3.2).
r "Введите число повторений: ", N
s time=0
f i=1:1:N {
s t0=$zh,t1=$zh,dt=t1-t0,time=time+dt
}
s time=time/N
w !,"Время исполнения $zh = ",time*10e6, " мкс"
q
| Повторов | 1 | 10 | 100 | 1000 | 10000 | 10**5 | 10**6 | 10**7 | 2*10**7 |
| Время (мкс) | 7 | 2,6 | 2,25 | 2,23 | 2,43 | 2,33 | 2,35 | 2,34 | 2,34 |
На моей машине в Windows XP время между двумя последовательно выполняемыми засечками времени составляет около 2,34 мкс.
Как показывают результаты экспериментальной проверки, полученное время между исполнениями $ZH не следует вычитать из общего времени, полученного в результате применения конструкции вида
$$s\verb*| |t0=$ZH\verb*| |исследуемая\_структура\verb*| |s\verb*| |t1=$ZH, dt=t1-t0$$
Это было бы необходимо, если бы задержка происходила только в результате исполнения программы. По-видимому, задержка определяется в основном ограниченными возможностями аппаратной части компьютера, не обеспечивающей более частые обращения к средствам хранения системного времени.
Для измерений длительности выполнения созданной вами программной конструкции вставьте её между командами S t0=$ZH и S t1=$ZH в программу в (листинге 3.37.
Теперь, изучив ObjectScript, мы может заняться организацией иерархических баз данных в Cache. Поскольку запросы в них имеют навигационный характер, необходимо освоить команды, позволяющие определить свойства узлов, организовать поиск в ширину и глубину.
Дополнительно будет рассмотрена команда копирования индексированных переменных, которая упростит групповые вставки.
Представим набор записей в виде массива, в котором имя массива — это имя набора, а имена индексов это имена полей записи. Значениями индексов в такой модели хранения данных будут значения полей. Если хотя бы один домен имеет мощность континуума, например, домен с вещественными числами, то ограничиться целочисленными индексами, как в массивах, изучаемых в математике, не удастся.
В языках общего назначения при определении массива с конечным набором значений индексов необходимо задать его размерность. Это необходимо, чтобы выделить область памяти, достаточную для хранения массива. Такие традиционные массивы будем называть плотными. В практически важных моделях данных такой массив сильно разрежен. Большую часть выделенного объёма занимают пустые места, соответствующие комбинациям значений индексов, которые не могут быть использованы или ещё не использованы.
Разреженные массивы, реализованные в Cache, отличаются следующими особенностями:
Вторая ипостась разреженного многомерного массива — это дерево. Нетрудно догадаться, что если присутствуют не все узлы, необходимые для образования дерева, то, может быть, придётся вводить в рассмотрение так называемые виртуальные узлы.
Рассмотрим пример. Создадим дерево в виде локала из следующих узлов: a(1)="A", a(2)="B", a(1,1,1)="C". В узле a(1)="A" единица —это индекс, "А" — значение узла a(1). Строго говоря, дерево получается (рисунок 3.22), только если считать, что к заданным узлам добавлен корневой узел a="" (его значение пустое, но могло быть любое другое) и промежуточный узел a(1,1). Последний называется виртуальным узлом. Такие узлы имеют имя и индексы, но не имеют значения. Их предназначение — связать узлы, отличающиеся на два и более индексов, чтобы на каждом следующем уровне дерева добавлялся ровно один индекс. Виртуальные узлы будем изображать двумя концентрическими окружностями.
(рис 3.22) Виртуальный узел а(1,1)
Не думайте, что виртуальные узлы вводятся в угоду теории. В следующем разделе станет понятно, что без них невозможна навигация по деревьям.
Замечание. Вы, конечно, заметили, что мы следуем противоестественной привычке математиков высаживать деревья вверх корнями.
Набор деревьев (лес) всегда можно свести в одно дерево или небольшое их число. На рисунке 3.23 представлены в экземплярах две связанные сущности "Команда" и "Игрок", ранее рассматривавшиеся в разделе 2.2.7 (рисунок 2.19).
Сущность "Команда" с экземплярами представлена на рисунке 3.23 деревом глубины 2. Заметим, что можно было бы ограничиться деревом глубины 1. В этом случае в нем на уровне 1 существовали бы узлы вида ^Команда (1) =" 1 "Спартак" или ^Команда (1)=$lb(" 1","Спартак") или что-нибудь ещё.
(рис 3.23) Представление двух связанных сущностей глобалами: Данные
Позже вы увидите, что именно так хранятся строки таблиц в реализации реляционной модели.
В варианте, представленном на рисунке 3.23рисунке 3.50, данные каждой сущности хранятся в своём дереве, а для всех метаданных использовано дополнительное дерево (рисунке 3.24). Поясним значения листовых узлов. РК—первичный ключ, NK —не ключ, FK—внешний ключ. Значения узла лМетадан-ные ( "Связь", "Связь1" ) представлено строкой с разделителем """
(рис 3.24) Представление двух связанных сущностей глобалами: Метаданные
Можно объединить все деревья в одно, выполнив необходимые переименования (рисунок 3.25)
(рис 3.25) Представление базы данных сущностей одним глобалом
Кроме констант и простых переменных узел может принимать следующие значения:
строка с разделителями, например,
K ^a S ^a(1)="a%b%c" W ^a(1)
список, например,
K ^a S ^a(1,1,1)=$LB("a","b")
W ^a(1,1,1),"~",$P(^a(1,1,1),2)
значение узла другого массива, например,
K ^b S ^b(1)="b1", ^b(2)="b2", ^b(1,1)="b11" S ^a(1,1,1)=^b(1) W ^a(1,1,1)
В соответствии с известным выражением, вы вольны расширять глобал в любую сторону своей души. Это не означает, конечно, что все варианты будут работать одинаково эффективно.
В результате создана база, содержащая данные и метаданные, организованные в иерархии. Система управления базой отсутствует. Все манипуляции с данными и метаданными и запросы данных придётся выполнять врукопашную. Контроль ограничений целостности не организован. Связь обозначена, но необходимо дописать процедурную часть, обеспечивающую её работу.
В иерархической модели данных для работы с базой необходимо, прежде всего, обеспечить навигацию по данным. Это означает, что всегда можно определить, в какой структуре, в каком элементе данных находится система управления базой и как попасть в нужное место. Для программной навигации необходимо хранить метаданные в словаре. В примерах предыдущего раздела метаданные хранились в глобалах, а процедурная часть словаря проработана не была.
В деревьях Cache навигация осуществляется на физическом уровне. Необходимо обеспечить движение в ширину (функция $ORDER) и движение в глубину (функция $QUERY). При конструировании глобалов их узлы создаются и удаляются командами SET и KILL. Копирование поддеревьев обеспечивает команда MERGE.
Существуют функции, поставляющие дополнительную информацию, необходимую для обеспечения навигации:
$DATA), сколько индексов у переменной (функция $QLENGTH);$QSUBSCRIPT);$GET)Начнём с получения вспомогательных данных.
Узлы можно характеризовать двумя признаками — наличием потомков и наличием значений. Если у узла нет потомков, но есть значение, то это листовой узел (на рисунке 3.22 — узлы a(2), a(1,1,1)). Если у узла есть потомки и значения, то это либо корень дерева, либо реальный промежуточный узел (узлы a, a(1)). Узел, имеющий потомков, но не имеющий значений — виртуальный (узел a(1,1)).
Тип узла определяет функция $DATA. Её значения сведены в таблицу 3.3
| Потомки | Значение узла | Код | Значение $DATA |
| 0 | 0 | 00 | 0 |
| 0 | 1 | 01 | 1 |
| 1 | 0 | 10 | 10 |
| 1 | 1 | 11 | 11 |
Cache не печатает ведущие (стоящие слева) нули. Поэтому возвращаются значения $DATA такие, как указано в последнем столбце таблицы.
В следующую таблицу 3.4 сведены варианты условий, проверяемых функцией $DATA.
| Условие | Значение $DATA | Запись условий |
| Потомки имеются | 10,11 | $DATA(узел)\10 |
| Значения имеются | 1,11 | $DATA(узел)#10 |
| Узел не существует | 0 | $DATA(узел)=0 |
| Узел виртуальный | 10 | $DATA(узел)=10 |
| Узел существует | 1,10,11 | $DATA(узел) |
Создадим локал с древесной структурой и проверим типы узлов с помощью функции $DATA (листинг 3.38).
USER>S a="", a(1)="A", a(2)="B", a(1,1,1)="C" USER>W $D(a),?10,$D(a(1)),?20,$D(a(2)),?30,$D(a(1,1,1)) 11 11 1 1 USER>W $D(a(1,1)) 10
Уровень узла, то есть число индексов в записи узла определяется функцией $QL[ENGTH]. Корень дерева имеет уровень 0 (у него нет индексов), на уровне 1 находятся узлы с одним индексом и т. д.
Для только что введенного дерева, просмотрим уровень всех узлов (листинг 3.39).
Функция $QL не просматривает реального узла, а только изучает текст своего аргумента. Поэтому можно определить глубину несуществующего узла несуществующего дерева (листинг 3.39рисунок 3.54, последняя командная строка).
USER>S a="", a(1)="A", a(2)="B", a(1,1,1)="C"
USER>W $QL("a"),?10,$QL("a(1)"),?20,$QL("a(1,1)")
0 1 2
USER>W $QL("a(1,1,1)")
3
USER>K W $QL("a(2,5)")
2
Функция $QS[UBSCRIPT] работает с полным именем переменной, в том числе извлекает индексы.
Полное имя включает имя области имён, которое окружается символами "|" и "|". Записывают его между знаком "^" и именем переменной с индексами (листинг 3.40).
Частные случаи команды:
$QS("имя",-1) возвращает окружение, если оно есть в полном имени;$QS("имя",0) возвращает имя;$QS("имя",n), где n > 1 возвращает индекс, если он есть, и пустую строку, если его нет.
USER>S ^a(1)=77
USER>ZN "%SYS"
%SYS>S a=^|"USER"|a(1) w a
77
%SYS>W $QS("^|""USER""|a(1)", -1)
USER
%SYS>W $QS("^|""USER""|a(1)", 0)
^a
%SYS>W $QS("^|""USER""|a(1)", 1)
1
%SYS>W $QS("^|""USER""|a(1)", 2)
%SYS>
Функция $G[ET] (аргумент) возвращает значение узла (листинг 3.41). Если аргумент отсутствует, вернётся пустая строка.
USER>K y S a="0" a(1)="B" USER>W "a=",$G(a),?10,"a(1)=",$G(a(1)),?20,"a(y)=",$G(y) a=0 a(1)=B a(y)=
Почти все команды или функции, обращающиеся к глобалам, изменяют указатель, называемый индикатором неполной глобальной ссылки. Он запоминает предка последнего узла, к которому производилось обращение.
В программе неполная глобальная ссылка записывается как знак """, за которым в круглых скобках следуют индексы, которые должны быть добавлены к индексам, хранящимся в индикаторе.
Примеры использования неполной глобальной ссылки приведены в листинге 3.42.
USER>S ^a="",^a(1)="A",^a(2)="B",^a(1,1)="C" USER>S ^a(1,1,1)="D",^a(1,1,2)="E" USER>S x=^a(1) w ^(2) B USER>S x=^a(1,1,1) w ^(2) E
Неполная глобальная ссылка становится не определённой после ссылки на неиндексированную переменную и после вызова функции $QUERY, которая будет описана в следующем разделе. Там же станет понятно, что неполная глобальная ссылка хорошо работает вместе с функцией $ORDER.
Последняя функция $NA[ME] возвращает имя с вычисленными индексами. Более точно,
$$\$NA(имя(i_1,...i_n), m)$$,
где $$m$$ —число, вернёт:
при $$m=0$$ —только имя без индекса;
при $$m>0, m \leq n$$ —имя с n индексами;
при $$m<0$$ —ошибка;
при $$m>n$$ —то же, что $$m=n$$.
Следует помнить, что обращения к узлу не происходит. Он может вообще не существовать.
Обратите внимание на то, что глобал при записи сокращается за счёт удаления повторяющихся значений индексов. Рассмотрим пример из документации Cache. Глобал:
^Data(1999) = 100 ^Data(1999,1) = "January" ^Data(1999,2) = "February" ^Data(2000) = 300 ^Data(2000,1) = "January" ^Data(2000,2) = "February"
будет храниться примерно в таком виде:
Data(1999):100|1:January|2:February|2000:300|1:January| 2:February|...
Такая запись может уменьшить количество читаемых блоков памяти.
Оба вида поиска в этом разделе будут демонстрироваться на глобалах. Однако всё сказанное применимо и к локальным многомерным массивам.
В разреженных массивах практически никогда нельзя предугадать следующий индекс на выбранном уровне индексации. Точно также не известны ни предыдущий индекс, ни первый, ни последний индексы на каждом уровне индексации.
Функция $O[RDER] (сокращённо $O) использует в качестве аргумента узел дерева и возвращает значение индекса следующего по порядку элемента. Первый аргумент даёт имя локала или глобала. Второй аргумент указывает направление обхода. Значение +1, оно же значение по умолчанию, определяет движение по возрастанию. Значение —1 задаёт движение по убыванию.
$$\$O[RDER](имя\_переменной, направление\_обхода)$$
Создадим глобал, изображённый на рисунке 3.26.
s ^G="",^G(-1)=-1,^G(1)=1,^G(7)=7,^G(-1,5)=-15 s ^G(-1,10)=-110,^G(7,"A")="7A",^G(7,"B")="7B"
Обратите внимание, что, например, команда W $O(^G(""),1)
вернёт индекс —1 крайнего левого узла на глубине 1. Команда $O(^G(7),1) вернёт пустой индекс.
Можно считать, что цепочка индексов каждого горизонтального подуровня замыкается на фиктивный пустой индекс, что изображено на на рисунке 3.26.
(рис 3.26) Функция $ORDER
Начнём движение с второго пустого индекса. Команда W $O^G(7, ""),-1) вернёт индекс крайнего правого узла в правом горизонтальном слое уровня 2. После выполнения команд W $O^G(7,"B"),-1) и $O^G(7,"A"),-1) получим пустой индекс. Цепочка замкнулась.
Для поиска самого левого индекса воспользуйтесь следующими функциями:
W $O^G(""),1) даёт крайний левый узел на первом уровне дереваW $O(^G(""),-1) даёт крайний правый узел на первом уровне дереваW $O(^G(-1,""),1) даёт крайний левый узел на втором уровне поддерева, образованного узлом ^G (-1).Обойти все узлы поддерева на одном уровне можно с помощью циклов, учитывающих имя узла, образующего поддерево, и замкнутость горизонтальных подуровней на узел с пустым индексом (листинг 3.43)
USER>S x="" F S x=$O(^G(x)) Q:x="" W !, x, ?30, ^G(x) -1 -1 1 1 7 7 USER>S x="" F S x=$O(^G(7,x)) Q:x="" W !,x,?30,^G(7,x) A 7A B 7B
Не забывайте, что после ключевого слова F в цикле без параметров и перед командой W стоят два пробела.
$0RDER пробегает все узлы уровня, включая виртуальные. Однако, приведенный цикл использовать без переделки нельзя, потому что, встретив виртуальный узел, не имеющий значения, он не сможет распечатать его.
Уточним детали. Сортируемые элементы располагают слева направо в порядке возрастания.
Вспомним естественный порядок сортировки. Сначала канонические числа, не содержащие избыточных нулей и знака "+", в порядке возрастания1. Затем неканонические числа. За ними идут слова, начинающиеся с букв латиницы. После них слова, начинающиеся с букв кириллицы.
Для уточнения порядка сортировки одиночных символов наберите следующие две строки
F i=1:1:255 S a($CHAR(i))=$CHAR(i) S x="" F S x=$O(a(x)) Q:x="" W !,x,?30,a(x)
и сами рассмотрите полученную последовательность. Обратите внимание на то, что буквы кириллицы расположены не подряд.
Замечание. Функция $CHAR(i) выдаёт символ с кодом i.
Поиск в глубину производится с помощью функции $Q[ERY], возвращающей в отличие от $0RDER, не следующий индекс, а имя следующего узла. Однако, возвращаются лишь узлы имеющие значение, то есть виртуальные узлы игнорируются. Движение по ним производится, но результаты не выдаются.
При обходе всего глобала начинаем движение с корня. Правила перемещения:
При этом необходимо помнить, что движение по горизонтали, как и для $0RDER возможно только по узлам одного подуровня, имеющим общего непосредственного предка.
Для проверки правила несколько изменим глобал ^G, добавив в него узел ^G(1,1,1) так, чтобы образовался виртуальный узел ^G(1,1) (листинг 3.44)
USER>S ^G="", ^G(-1)=-1, ^G(1)=1, ^G(7)=7, ^G(-1, 5)=-15 USER>S ^G(-1, 10)=-110, ^G(7, "A")="7A" USER>S ^G(7, "B")="7B", ^G(1,1,1)=111 USER>S x="^G" F S x=$Q(@x) Q:x="" W x, "=", @x, ! ^G(-1)=-1 ^G(-1, 5)=-15 ^G(-1, 10)=-110 ^G(1)=1 ^G(1,1,1)=111 ^G(7)=7 ^G(7, "A")=7A ^G(7, "B")=7BF
После узла ^G(1) был пройден виртуальный узел ^G(1,1), но поскольку он не имеет значения, в выходных данных сведений о нём нет. При следующем повторе цикла отработано движение вниз к узлу ^G(1,1,1).
Мнемоническое изображение правил поиска в глубину с использованием функции $QUERY и подробное обписание обхода дерева ^G из предыдущего примера приведены на рисунке 3.27.
(рис 3.27) Функция $QUERY
Команда MERGE позволяет вклеивать в индексированные переменные копии других индексированных переменных, независимо от того, являются ли они локальными или глобальными. Синтаксис похож на синтаксис присваивания:
$$M[ERGE]\verb*| |переменная\_результат = вставляемая\_переменная$$
Для проверки результатов этой операции удобно воспользоваться программой просмотра глобала ^%G (листинг 3.45). При первом её исполнении в ответ на предложение ввести устройство и ширину строки нажимайте на Enter, вставив значения по умолчанию, затем введите имя глобала. После ввода пустого имени программа прекращает работу.
USER>D ^%G Device: Right margin: 80 => Screen size for padding (0=nopadding) ? 24 => For help on global specifications DO HELP^%G Global ^G ^G "" ^G(-1)=-1 ^G(-1,5)=-15 10)=-110 ^G(1)=1 ^G(1,1,1)=111 ^G(7)=7 ^G(7,"A")=7A "B")=7B Global ^
Повторы значений программа не печатает. Так что запись " 10)=-110" после записи <^G(_l,5)=-15" следует читать как "^G(_1,10)=-110".
Создадим глобал с узлами ^a(l), ^a(2), ^a(l,l) и локал b("a"), b("b"). Вставим локал b в узел ^a(1) глобала и просмотрим результат операции (листинг 3.46)
Приведённые в главе 3 сведения о языке Cache ObjectScript достаточны для создания иерархических баз данных, манипуляций данными и выполнения запросов.
В практической работе должны использоваться средства для администрирования базы данных, которые мы, к сожалению, не изучаем.
USER>S ^a="", ^a(1)="", ^a(2)="", ^a(1, 1)=""
USER>S b("a")="", b("b")=""
USER>MERGE ^a(1)=b
USER>D ^%G
For help on global specifications DO HELP^%G
Global ^a
^a
""
^a(1)=""
^a(1,1)=""
"a")=""
"b")=""
^a(2)=""
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.