Введение

RPG - это совсем не то, что приходит на ум обычному человеку. Это не только ролевые игры (Role Play Games), но еще и язык программирования. Здесь уже была статья Первый взгляд на RPG: оказывается, это не только ролевые игры, но она носила достаточно обзорный характер и не отражала современного состояния дел.

Будучи “широко распространенным в узких кругах” (достаточно сказать что более 80% кода на платформе IBM i написано и пишется именно на RPG), это язык практически не используется за пределами этой платформы.

Да, были попытки распространить этот язык за пределы его родной платформы - компания ASNA выпускала свою версию Visual RPG (в т.ч. и для .NET), но широкого распространения все это не получило.

Стоит сразу отметить, что RPG не является ЯВУ общего назначения. Это специализированный язык для работы с БД и коммерческих расчетов. Именно так его и надо воспринимать. И тут сравнения “что лучше - RPG или C/C++/C#/Rust/[подставить по вкусу]” бессмысленны (и далее, где упоминается С - это сравнение не языков, но концепций, просто С достаточно распространен и многим знаком). RPG создавался и развивался для решения вполне определенного класса задач и там он хорош - эффективен и прост. Для решения иных задач он будет крайне неудобным.

Данная статья ни в коей мере не является пособием или референсом по языку. Здесь опущены многие моменты и не дается полного описания всех функций и особенностей. Это просто попытка в общих чертах обрисовать что из себя представляет современный RPG, где, почему и зачем он используется. Если где-то покажется, что технические детали разбираются слишком сильно - это исключительно для того, чтобы подчеркнуть особенности языка в плане удобства и эффективности решения поставленных перед ним задач.

Немного истории

Когда-то давным-давно, когда компьютеров еще не было, использовались разного рода механические счетные машины. И среди них были так называемые “табуляторы” которые умели принимать входную информацию с перфокарт. С появлением ЭВМ возникло желание использовать то, что было уже накоплено для табуляторов, на более высоком уровне.

Первая версия языка - эмулятора табуляторов была выпущена IBM в 1959-м году для IBM-1401. Назывался этот язык Report Program Generator (RPG). 

Естественно, что первая версия была крайне скудна возможностями и, кроме того, имела т.н. “позиционный синтаксис” (FIXED) и выглядела, мягко говоря жутковато:

      * "F" (file) specs define files and other i/o devices
     F ARMstF1   IF   E       K     Disk    Rename(ARMST:RARMST)
      * "D" (data) specs are used to define variables
     D pCusNo          S              6p
     D pName           S             30a
     D pAddr1          S             30a
     D pAddr2          S             30a
     D pCity           S             25a
     D pState          S              2a
     D pZip            S             10a
      * "C" (calculation) specs are used for executable statements
      * Parameters are defined using plist and parm opcodes
     C     *entry        plist
     C                   parm                    pCusNo
     C                   parm                    pName
     C                   parm                    pAddr1
     C                   parm                    pAddr2
     C                   parm                    pCity
     C                   parm                    pState
     C                   parm                    pZip
      * The "chain" command is used for random access of a keyed file
     C     pCusNo        chain     ARMstF1
      * If a record is found, move fields from the file into parameters
     C                   if        %found
     C                   eval      pName  = ARNm01
     C                   eval      pAddr1 = ARAd01
     C                   eval      pAddr2 = ARAd02
     C                   eval      pCity  = ARCy01
     C                   eval      pState = ARSt01
     C                   eval      pZip   = ARZp15
     C                   endif

Связано это было с тем, что терминалов, за которыми работают программисты, тогда еще не было. Программы писались на специальных бланках

и отдавались операторам для переноса на перфокарты.

Следующее поколение языка, RPG II появилось в 1969-м году и предназначалось для IBM System/3 где стало “основным языком” (поскольку эти машины изначально позиционировались как коммерческие компьютеры). Дальше железо развивалось - появились System/32, System/34, System/36 и, наконец, System/38, на которых была реализована следующая версия - RPG III. Но он все еще оставался fixed style…

На основе System/38 была создана новая версия платформы - AS/400 (которая, фактически, жива до сих пор и развивается, но уже, пройдя ряд переименований, IBM i). На ней уже была следующая модификация - RPG/400.

И, наконец, в 1994-м году, появился RPG IV, он же ILE RPG. В этой версии произошла революция - появился “свободный” (free) стиль синтаксиса и язык стал нормальным процедурным языком с человеческим синтаксисом.

А ILE - это отдельная фишка IBM i - интегрированная языковая среда. Если в двух словах, суть ее в том, что вы можете написать несколько “модулей” (*MODULE - в IBM i это аналог объектного файла .o или .obj - результат работы компилятора) на разных языках и затем объединить (тут это называется bind - то же самое что link) их в одну программу. И, естественно, вызывать функции, написанные на одном языке из кода на другом. В настоящее время в группу ILE языков на IBM i входят CL, COBOL (да, он тут до сих пор поддерживается, хотя и крайне редко используется), C, C++ и RPG.

Что сегодня?

Как уже было сказано выше, сегодня RPG является вполне современным процедурным языком. Ну примерно на уровне классического Паскаля. И при этом непрерывно развивается - постоянно добавляются новые возможности, встроенные функции и т.п.

В отличии от многих других языков, RPG является “самодостаточным” для решения поставленных перед ним задач. Иными словами, он не требует привлечения каких-либо “зависимостей”, сторонних библиотек, фреймворков. Для работы с БД и коммерческих вычислений в нем есть все что нужно.

Переменные

RPG является языком со сильной (ну, по крайне мере так утверждается) статической типизацией.

В некоторой степени он позволяет неявное преобразование типов (где это возможно).

Типов данных в языке достаточно много - помимо “классических” типов, таких как char, int, float, он поддерживает все типы данных, которые содержатся в БД.

Все имена в RPG регистронезависимые (ну так исторически сложилось - старый код вообще практически весь в UPPERCASE написан). 

В именах переменных разрешены некоторые спецсимволы - опять же, в старом коде можно встретить (почему-то это было модно когда-то) переменные с именами @CNT, #CNT и т.п.

Типы данных

Числовые

  • int - целое, может быть 1, 2, 4 или 8 байт. Соответствует smallint, integer, bigint в SQL

  • uns - беззнаковое целое, 1, 2, 4 или 8 байт

  • bindec - Двоично-десятичный формат.

  • float - с плавающей точкой, 4 или 8 байт. Соответствует float в SQL

  • packed - packed decimal - с фиксированной точкой, до 32 байта (63 знака). Соответствует decimal в SQL

  • zoned - zoned decimal - с фиксированной точкой, до 63 байт (знаков). Соответствует numeric в SQL

Есть особенности в объявлении численных переменных - целочисленные объявляются с указанием количества максимального знаков (не байт!). Т.е. int(3) - 8-битное знаковое целое, int(5) - 16-битное, int(10) - 32-битное, int(20) - 64-битное. Других вариантов для int нет. Только 3, 5, 10 или 20. Для uns - аналогично.

Для float же указывается размер в байтах - float(4) или float(8).

Строковые

  • char - строка символов. Если строка заполнена не до конца, “хвост” заполняется пробелами. Соответствует char в SQL

  • varchar - строка символов перед которой идет поле (2 или 4 байта), содержащее фактическую (заполненную) длину строки. Соответствует varchar в SQL

  • ucs2 - строка двухбайтовых символов, аналогично char. Соответствует graphics в SQL

  • varucs2 - строка двухбайтовых символов, перед которой идет поле длины, аналогично varchar. Соответствует vargraphics в SQL

Тут надо отметить, что строка (char) в RPG не является null-terminated. Строка типа char не имеет какого-то ограничителя, кроме длины. Если длина присваемой строки меньше длины объявленного буфера, остаток заполняется пробелами. "Пустой" считается строка, полностью заполненная пробелами. Если же длина присваемой строки больше длины объявленного буфера, она будет обрезана до длины буфера.

Надо помнить, что функция %len для char строки всегда будет возвращать ту длину, которая была в объявлении переменной. Для получения "реальной" длины (без "пустого хвоста") нужно использовать %len(%trimr(str)) т.е. длину обрезанной справа строки.

Иногда бывает удобно при работе со строками использовать команду evalr - присвоение с правым выравниванием

dcl-s str1 char(10) inz('0123456789');
dcl-s str2 char(5);
dcl-s str3 char(5);

str2 = str1;       // str2 = '01234' - автоматическая обрезка
evalr str3 = str1; // str3 = '56789' - зполнение строки-результата "с конца"

Более эффективно использовать varchar cтроки, которые фактически представляют из себя структуру

typedef struct {
    unsigned short len;
    char data[N];
} VARCHAR_T;

где N - количество символов в объявлении.
dcl-s str varchar(N);

Для длинных строк допустимо объявление
dcl-s str varchar(N: 4);
в этом случае поле len будет не unsigned short, а unsigned int.

Для varchar строк функция %len возвращает реальную длину - значение поля len, Более того, %len для varchar может использоваться и как lvalue - %len(str) = 0 установит значение len в 0 (пустая строка).

Логические

  • ind - индикатор. Фактически тождественен char(1), принимающий значения ‘1’ или ‘0’ (мнемоники *on и *off соответственно)

Дата, время

  • date - дата. Соответствует date в SQL

  • time - время. Соответствует time в SQL

  • timestamp - дата + время (до микросекунд). Фактически - строка 26 символов (с разделителями). Соответствует timestamp в SQL

Указатели

  • pointer - указатели не типизированы. Точнее, есть два типа указателей - указатели на область данных - pointer и указатели на процедуры - pointer(*proc)

Также возможно описание переменных со ссылкой на SQL тип, например, объявление переменной var, соответствующей SQL типу CLOB размером 65536 байт

dcl-s var sqltype(CLOB: 65536);

Объявление переменных, модификаторы

Объявление переменных всегда начинается с dcl-s - declare standalone variable. Далее идет имя переменной, затем тип. После чего список необязательных модификаторов:

  • ccsid - для строковых переменных указание на то, что кодировка отличается от дефолтной. Например,
    dcl-s strUTF8 char(50) ccsid(1208);
    объявляет строку 50 символов в кодировке utf8

  • static - статическая переменная

  • inz - инициализация переменной неким значением. Например,
    dcl-s i int(5) inz(10);
    объявляет 2-байтовое (5 знаков) целое и инициализирует его значением 10.
    Может быть просто inz, без значения, тогда инициализация значением по умолчанию для данного типа.

  • dim - размерность для массива. Например,
    dcl-s arr char(5) dim(10);
    массив из 10-ти строк по 5 символов.

  • template - указание на то, что это не переменная, а шаблон, аналогично typedef в C. Далее этот шаблон можно использовать для объявления переменных “таких же как …”. Например,
    dcl-s t_CUS char(6) template;
    dcl-s CUS1 like(t_CUS);
    dcl-s CUS2 like(t_CUS);
    сначала объявили шаблон t_CUS, затем - две переменных CUS1 и CUS2 “таких же как t_CUS”.
    В like может стоять не обязательно шаблон, но имя любой объявленной ранее переменной.

  • based - типизация указателя. Как уже сказано выше, указатели в RPG не типизированы. Чтобы как-то использовать указатель, необходимо его предварительно “типизировать”. Т.е. описать, на что именно он указывает с тем, чтобы потом можно было с этим как-то работать. Например,
    dcl-s r_Str char(50) based(pStr);
    описывает строку 50 символов, связанную с указателем. После инициализации указателя (присвоения ему адреса какого-то участка памяти) работать с ней можно как с обычной строкой.

  • ascend/descend - модификаторы для массивов. Служат исключительно для того, чтобы функция поиска по массиву использовала алгоритм быстрого (двоичного) поиска, а не искала перебором. Естественно, что после заполнения и перед поиском массив нужно отсортировать (по возрастанию если используется модификатор ascend или по убыванию если descend).
    Ограничение - эти модификаторы не могут применяться к массивам структур данных.

  • import/export - служит для импорта/экспорта переменных между модулями (когда программа собирается более чем из одного исходника)

В RPG не бывает неинициализированных переменных. Компилятор всегда инициализирует переменную значением по умолчанию для данного типа (если явно не указано значение для инициализации модификатором inz).

Константы

Константы объявляются при помощи dcl-c
dcl-c myConst 'My Const';
или
dcl-c myConst const('My Const');

Константы не типизированы, точнее, автоматически типизированы
dcl-c myConst 'MyConst';
строковая константа, а
dcl-c myConst 6;
числовая.

Перечисления

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

Перечисление представляет из себя группу констант, объединенную в блок dcl-enum
dcl-enum Colors;
red 'RED';
green 'GREEN';
blue 'BLUE';
end-enum;
Обращение к элементам перечисления производится в данном случае как к обычным константам - red, green...
Также перечисление может быть объявлено с модификатором qualified -
dcl-enum Colors qualified;
В этом случае к его элементам обращаться нужно по "полному имени" - Colors.red, Colors.green...

Все элементы перечисления должны быть однотипны - нельзя в одном перечислении одновременно использовать строковые и числовые литералы.

Перечисление может быть использовано для инициализации массива:
dcl-s arrColors char(5) dim(3);
arrColors = Colors;

Также перечисление может использоваться в проверке условия
if var in Colors;
или в цикле for-each
for-each var in Colors;

Мнемоники

Для переменных есть несколько мнемоник:

  • *on/*off - для индикаторов и результатов логических выражений (аналог true/false)

  • *blank/*blanks - для char - “пробел” (один) или “заполнено пробелами” (вся строка). Например,
    if str = *blanks;
    если строка пустая (полностью заполнена пробелами).

  • *zero - 0 для числовых типов

  • *loval/*hival - очень полезные мнемоники. Работают для любого типа данных. Означают “минимально возможное” и “максимально возможное” значение для данной переменной (данного типа и размера). Например,
    dcl-s pktVal packed(5: 0) inz(*hival);
    объявляет переменную с фиксированной точкой содержащую 5 символов и инициализирует ее максимально возможным для нее значением 99999. Не надо помнить “а сколько там максимально возможное для …, а где оно объявлено, а как называется та константа”. Просто пишем *hival и компилятор уже сам разберется.

Сброс переменных к начальному значению

Есть два полезных оператора для работы с переменными:

  • clear - сброс значения переменной к дефолтному для данного типа. Например,
    dcl-s str char(10);

    clear str;
    Переменная объявлена как char. Дефолтное значение - пробел. Т.е. после clear получаем строку 10 символов, заполненную пробелами.

  • reset - сброс значения переменной к указанному при объявлении. Если не указано особого значения inz, то работает аналогично clear. Иначе - к тому, что указано в inz. Например,
    dcl-s str char(50) inz(‘Just a string’);

    reset str;
    после reset строка принимает значение ‘Just a string’ как было указано в inz.

Массивы

В целом, тут нет ничего особенного, все как у всех. Ну разве что массивы нумеруются с 1, а не с 0 как в C/C++.

Но, есть...

Динамические массивы

В RPG они появились сравнительно недавно.

Есть два типа динамических массивов:

  • с ручным управлением выделением памяти
    dcl-s array int(10) dim(*var: 1000);
    объявляет динамический массив с максимальным размером 1000 элементов. Управление текущим количеством элементов осуществляется при помощи BIF (built-in-function) %elem которая может быть как lvalue, так и rvalue
    n = %elem(array);
    возвращает количество элементов в массиве, а
    %elem(array) = n;
    устанавливает размер массива в n элементов.
    Если массив объявлен как var и размер его 5 элементов, а вы попытаетесь что-то записать в 6-й - получите ошибку.

  • с автоматическим выделением памяти
    dcl-s array int(10) dim(*auto: 1000);
    здесь тоже работает %elem, но в отличии от *var массива, если размер 5 элементов, а вы попытаетесь что-то записать в 6-й - он просто увеличит размер и ошибки не будет. Более того, можно так:
    dcl-s array int(10) dim(*auto: 1000);
    %elem(array) = 0;
    for i = 1 to 1000;
    array(*next) = i;
    endfor;
    сначала сбрасываем массив в 0 элементов, а потом заполняем его, записывая каждое новое значение в следующий после текущей границы массива элемент (добавляя каждый раз новый элемент к массиву)

Есть ограничение - динамические массивы не могут быть элементов структуры данных.
Кроме того, есть особенность с передачей динамических массивов аргументом в процедуры. Во-первых, если предполагается что в процедуру будет передаваться динамический массив, аргумент должен описываться как обычный массив с размерностью равной максимальной размерности динамического массива с options(*varsize). При этом внутри процедуры не будет работать %elem - оно всегда будет возвращать максимальное число элементов вне зависимости от того, сколько их там реально в массиве.

Т.е. если хотим передавать в myProc динамический массив

dcl-s  dynArray  char(6)  dim(*auto: 1000);

то прототип процедуры должен выглядеть так:

dcl-pr myProc;
  dynArray  char(5) dim(1000)  options(*varsize);
  elmCnt    int(10) value;
end-pr;

а вызываться она будет так:

myProc(dyArray: %elem(dynArray));

Возможно, в будущих реализациях это будет улучшено - чуть не в каждом TR (Technology Refresh - минорное обновление системы, появляется в среднем пару раз в год) что-то новое по языку добавляется.

Структуры данных

А вот тут все достаточно интересно. Концепция структур в RPG немного отличается от C. RPG рассматривает структуру как байтовый буфер (строку), внутри которого размечены поля. В простейшем случае - все как в С - поля следуют одно за другим, размер структуры равен сумме размеров полей. Но это только в простейшем случае.

На самом деле можно явно задать размер структуры большим, нежели сумма размеров ее полей. А для каждого поля внутри структуры явно задать его положение (смещение от начала). При этом никто не запрещает полям накладываться друг на друга. Или частично пересекаться друг с другом. Т.е. структура в RPG перекрывает как структуры, так и юнионы в С. Вообще, это очень гибкий инструмент для описания всевозможных данных.

Объявление структуры начинается с dcl-ds (здесь все объявления начинаются с dcl-...) и заканчивается end-ds. Простейшая структура выглядит так:
dcl-ds dsMyDS;
field1  char(10);
field2  packed(15: 0);
end-ds;

Модификаторы для объявления структур

  • qualified - модификатор, означающий что к полям структуры нужно обращаться по полному имени - dsMyDS.field1. Без этого модификатора обращение идет просто по имени поля - field1 (да, это может вносить путаницу, поэтому всегда используем этот модификатор)

  • len - модификатор, явно задающий размер структуры в байтах (может быть больше суммы размеров ее полей). Например,
    dcl-ds dsMyDS len(64) qualified;
    field1 char(10);
    field2 char(20);
    end-ds;
    сумма размеров полей - 30 байт, но размер структуры будет 64 байта ибо так указано в len

  • likerec - это специальный модификатор, указывающий на то, что структура соответствует заданному формату записи в таблице БД. Только такие структуры могут быть использованы в операциях прямой работы с таблицами. Например,
    dcl-ds dsMyRec likerec(MYFILE.MYFILERECF: *all);
    означает что структура dsMyRec соответствует формату записи MYFILERECF в таблице MYFILE. *all тут значит “все поля” (может быть еще *key - только ключевые поля). Объявленные таким образом структуры автоматически являются qualified. Для такого объявления предварительно должен быть объявлен сам файл (таблица) MYFILE

  • ext extname - также модификатор, ссылающийся на структуру записи в таблице. В отличии от likerec не требует предварительного объявления самой таблицы, но такие структуры не могут использоваться в операциях с БД. Например,
    dcl-ds dsMyRec ext extname('MYFILE': *all) qualified;
    такие структуры не являются qualified по умолчанию.

  • dtaara - модификатор, связывающий структуру с системным объектом Data Area (*DTAARA) на IBM i. Если кратко, то это аналог файла произвольного содержания, размером не более 2000 байт. Обычно используется для хранения разного рода настроек. Например,
    dcl-ds dsMyDtaAra dtaara('MYDTAARA') len(512) qualified;
    связывает структуру с объектом MYDTAARA типа *DTAARA. Здесь обязательно использование len с указанием размера, равного размеру data area (в данном случае - 512 байт). Такие структуры можно использовать в операциях чтения-записи DTAARA.

  • align - по умолчанию элементы структуры не выравниваются на границу слов. Если требуется выравнивание, используется модификатор align (или align(*full) - более сильное выравнивание)

также могут использоваться модификаторы, используемые для обычных переменных: dim (массив структур с тем исключением, что для массивов структур недопустимы модификаторы ascend/descend - это доставляет некоторые неудобства т.к. есть возможность отсортировать массив по одному или нескольким полям и функции поиска по массиву позволяют искать по значению конкретного поля , но вот поиск все равно будет только перебором - компилятор использует быстрый двоичный поиск только по массивам с модификаторами ascend/descend), template (разница в том, что структура по шаблону объявляется не как like, а как likeds), based, import/export.

Возможности структур в RPG

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

dcl-ds dsMyDS qualified;
	field1 char(5);
	field2 char(5);
	field3 char(5) pos(3);
end-ds;

здесь модификатор pos указывает на позицию (от начала, нумерация с 1) поля в структуре. И если мы напишем

dsMyDS.field1 = ‘01234’;
dsMyDS.field2 = ‘56789’;

то dsMyDS.field3 автоматически будет равно ‘23456’.

Можно совмещать поля разных типов - это не контролируется компилятором и вся ответственность за такие трюки исключительно на совести разработчика. Но иногда это бывает полезно. Например,

dcl-ds dsMyDS qualified;
	field1 char(7);
	field2 zoned(7: 0) samepos(field1);
end-ds;

(модификатор samepos означает “в той же позиции”) тут используется тот факт, что тип zoned, хоть и является числовым, но для положительных чисел в памяти его представление тождественно строке. Т.е. в используемой по умолчанию на IBM i для русского языка кодировке EBCDIC 1025 цифры имеют коды с 0xF0 по 0xF9 что совпадает со способом хранения положительных чисел в формате zoned.

Иными словами, dsMyDS.field2 = 56 даст нам в dsMyDS.field1 строку ‘0000056’.

Надо отметить, что обратное преобразование рискованно - если в field1 записать ‘56’, то обращение к field2 вызовет системное исключение “не является числом” т.к. на самом деле это поле в памяти будет выглядеть как ‘56     ‘  (0xF5 0xF6 0x40 0x40 0x40 0x40 0x40 в используемой на IBM i кодировке EBCDIC 1025) что не совпадает с представлением zoned чисел. Надо писать именно ‘0000056’ (0xF0 0xF0 0xF0 0xF0 0xF0 0xF5 0xF6).

Есть еще вариант наложения для массивов. Например, есть ПИН клиента, 9 символов, который состоит из двух частей - 6 символов CUS + 3 символа CLC. И мы можем описать такую вот конструкцию:

dcl-ds dsMyDS qualified;
  arrPIN   char(9) dim(10);
    arrCUS char(6) overlay(arrPIN);
    arrCLC char(3) overlay(arrPIN: *next);
end-ds;

Такая запись позволяет нам обращаться непосредственно к составляющим arrPIN как к отдельным элементам массива. И если dsMyDS.arrPIN(5) = ‘AAAAAA001’, то dsMyDS.arrCUS(5) будет равен ‘AAAAAA’, а dsMyDS.arrCLC(5) будет равен ‘001’.

Но и это еще не все. Структуры поддерживают неименованные поля. Те, что в С-шных структурах служат для разного рода выравниваний и именуются reserved, dummy, tmp и т.п. Тут просто вместо имени поля пишем *n

Объявление структуры может делаться шаблоном (модификатор template). И в этом шаблоне можно сразу указывать значения для инициализации полей (это можно делать для любой структуры, не только для шаблона, но в шаблонах это особенно полезно). Например:

dcl-ds t_ISODateStr qualified template;
	year  char(4)  inz(‘2000’);
	*n    char(1)  inz(‘-’);
	month char(2)  inz(‘01’);
	*n    char(1)  inz(‘-’);
	day   char(2)  inz(‘01’);
	str   char(10) samepos(year);
end-ds;

Теперь, если мы объявим структуру по этому шаблону (likeds) и проинициализируем ее как прописано в шаблоне (inz(*likedes))

dcl-ds dsISODateStr likeds(t_ISODateStr) inz(*likeds);

то в поле dsISODateStr.str будет сразу содержаться ‘2000-01-01’. Далее, мы можем заполнять поля year, month и day и в поле str иметь сразу дату. А если использовать указанный выше трюк с zoned:

dcl-ds t_ISODateStr qualified template;
	year  zoned(4:0)  inz(2000);
	*n    char(1)     inz(‘-’);
	month zoned(2:0)  inz(1);
	*n    char(1)     inz(‘-’);
	day   zoned(2:0)  inz(1);
	str   char(10)    samepos(year);
end-ds;

то записывая в поля year, month и day числа, в поле str будем иметь дату в виде строки (причем, с автоматическим дополнением нулями слева - записав в поле month значение 1, в строке увидим 01):

dsISODateStr.year  = 2024;
dsISODateStr.month = 5;
dsISODateStr.day   = 8;

даст нам строку dsISODateStr.str = '2024-05-08'

А если после всех манипуляций сделать reset dsISODateStr, то значения всех ее полей вернутся к тем, что прописаны в шаблоне…

При работе со структурами часто бывает полезна команда eval-corr - присвоение значений полей одной структуры, полям другой структуры с такими же именами

dcl-ds ds1 qualified;
  fld1  char(5);
  fld2  char(5);
  fld3  char(5);
end-ds;

dcl-ds ds2 qualified;
  fld1  char(5);
  fld3  char(5);
  fld4  char(5);
  fld5  char(5);
end-ds;

eval-corr ds2 = ds1;

что эквивалентно

ds2.fld1 = ds1.fld1;
ds2.fld3 = ds1.fld3;

Циклы и ветвления

Тут, собственно говоря, нет ничего особенного. Все по классике.

Ветвления

Ветвление по условиям осуществляется либо конструкцией if

if <условие1>;
  [elseif <условие2>];
  [else];
endif;

В качестве условия могут использоваться традиционные =, >, <, >=, <=, <> или in - проверка вхождения в диапазон, список, массив или перечисление:

if a in %range(1: 10);                     // значение a в диапазоне от
                                           // 1 до 10
if word in %split(string: ' ');            // значение word входит в
                                           // строку string где слова
                                           // разделены пробелами
if color in %list('RED': 'GREEN': 'BLUE'); // color имеет одно из трех
                                           // значений - ‘RED’, ‘GREEN’
                                           // или ‘BLUE’
if a in array;                             // значение a присутствует в
                                           // массиве array

Естественно, возможны сочетания условий по and и/или or или отрицание not.

Немного интереснее реализована конструкция выбора из нескольких вариантов - select. Она может использоваться как без операнда

select;
  when <условие1>;
  when <условие2>;
  [other;]
endsl;

где правила на условия точно такие же как в  if (фактически полный аналог if... elseif ... elseif ... endif), так и с операндом (близко к switch в C, но операнд может быть любого типа - строка, дата и т.п.)

select <переменная>;
  when-is <значение>;
  when-in <список, массив, диапазон илои перечисление>;
  [other;]
endsl;

Тут есть отличие от switch в С. Заключается оно в том, что select находит первое по списку подходящее условие, выполняет стоящие за ним операции и выходит. Дальше оно не пойдет (т.е. не нужен break как в С).

А вот goto тут нет. Совсем. Оно было в старых версиях (fixed формат), в новых (free) его исключили.

Циклы

Циклы могут быть

for <параметр цикла> [= <начальное значение>] [by <шаг>] to|downto <конечное значение>;
  ...
endfor;

если начальное значение не указано, используется то, что было на момент входа в цикл.

Также есть цикл

for-each <параметр> in <список, массив или перечисление>;
  ...
endfor;

например

for-each word in %split(string: ' ');
  или
for-each color in %list('RED': 'GREEN': 'BLUE');
  или
for-each a in array;

Также есть циклы do-while

dow <условие>;
  ...
enddo;

который выполняется пока условие истинно (если оно изначально ложно - не выполняется ни разу), и do until

dou <условие>;
  ...
enddo;

который выполняется пока условие не станет истинным (и при этом первый раз цикл выполнится всегда - условие начинает проверяться только после первого прохода, не самый удачный синтаксис на мой взгляд т.к. визуально условие в начале цикла, но проверяется в конце...).

Принудительный выход из цикла осуществляется командой leave, принудительный переход из середины на начало цикла - командой iter.

Команды и встроенные функции

Несколько странным наследием прошлого в RPG остается разделение на команды (операторы, или, как их называют, opcodes) и встроенные функции (тут их называют BIF’ы - Built-In-Fuction).

Разделение идет (предположительно) по принципу - если что-то делает и ничего не возвращает - команда. Если возвращает - BIF.

Стоит отметить, что RPG является “самодостаточным языком” - чтобы на нем делать то, для чего он предназначен (работа с БД и бизнес-логика в т.ч. финансовые расчеты), не требуется никаких дополнительных зависимостей. Все что нужно, есть в самом языке.

Команды

Синтаксис у команд и функций разный. Если у функций все традиционно (ну не считая того, что все имена BIF’ов начинаются с символа %) - аргументы в скобках, разделяются двоеточием (еще одна особенность RPG - тут вместо традиционного разделителя - запятой используется двоеточие), то в командах аргументы без скобок и через пробел. При этом есть команда open (открытие файла), а есть функция %open - возвращает открыт файл или нет. Довольно часто используемая конструкция:

if not %open(myFile);
  open myFile;
endif;

BIF’ы

Как уже сказано выше, имена всех встроенных функций начинаются с символа %. Каждая функция что-то возвращает. Более того, есть функции, которые могут быть как rValue, так и lValue. Например, функция для работы с подстроками. Если написать
str2 = %substr(str1: 3: 5);
то в str2 будет скопировано 5 символов из str1 начиная с 3-го.
Если str1 = ‘0123456789’, то в str2 будет ‘23456’

А если 
%substr(str1: 3: 5) = str2;
то первые 5 символов из str2 будут скопированы в str1, начиная с 3-го…
если str1 = ‘0123456789’, а str2 = ‘01234’, то после этой операции в str1 будет ‘0101234789’ 

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

Опять же, перечисление всех встроенных функций языка не входит в задачу данной статьи, достаточно сказать, что их набор полностью покрывает все поставленные перед языком задачи. В наличии богатый арсенал функций для работы со строками (разбиение строки на массив элементов, склейка строки из массива элементов, поиск в строке, поиск с заменой и т.д. и т.п.). Также широко представлены функции работы с датами - представление в виде строки или числа в заданном формате или конвертация строки или числа в дату, арифметика с датами (разность дат, увеличение/уменьшение даты на заданное количество дней/месяцев/лет, выделение части даты и т.п.). Аналогично для времени. Есть функции поиска в массиве (причем, поиск может вестись не только по равенству, но по любому условию - не равно, больше, меньше, меньше или равно, больше или равно). Работает это в том числе и для массивов структур с указанием к какому полю структуры применяется условие поиска.

Процедуры

С рождения в RPG не было процедур. Там вообще была интересная концепция “циклического выполнения программы” (cycle mode), ориентированная на то, что программа будет брать файл (таблицу) и по очереди обрабатывать все записи в ней одну за другой. Там не было оператора return, а выход из программы осуществлялся установкой индикатора LR (Last Record) в состояние *on. Пока индикатор не установлен, программа, дойдя до конца, будет возвращаться на начало.

Все переменные в то время были глобальными, а единственным средством структурирования кода в то время были подпрограммы (“сабрутины”, subroutbnes) как в классическом BASIC - без аргументов, без локальных переменных, без отдельного уровня стека.

Естественно, такая концепция накладывала слишком много ограничений и с развитием языка от нее отказались.

Описание процедуры

Сейчас RPG - нормальный процедурный язык, работающий в linear mode (как любой другой). Если сравнить с тем же С, то есть ряд синтаксических отличий

  • Точкой входа в программу может быть любая из процедур. Определяется она в первых строках программы директивой ctl-opt (control option). если главная процедура называется MainProc, то выглядит это так:
    ctl-opt main(MainProc);

  • Главная процедура ничего не возвращает 

  • Аргументы главной процедуры могут быть любыми (а не argc/*argv[] как в С)

  • Компилятор не является однопроходным - объявление процедуры или ее прототипа не обязательно должно предшествовать первому ее вызову.

Но механизм подпрограмм при этом сохранился. Т.е. внутри процедур их можно по прежнему использовать что иногда бывает удобно для выделения повторяющихся блоков кода и/или выделения логических блоков в рамках процедуры.

Тело процедуры начинается с dcl-proc и заканчивается end-proc. Если процедура возвращает какое-то значение или имеет аргументы, внутри блока dcl-proc должен присутствовать блок объявление “интерфейса” - dcl-pi … end-pi

dcl-proc myProc;
  dcl-pi *n char(5);
    par1   char(5) const;
	daPar2  likeds(t_daPar2);
  end-pi ;

  dcl-s  retVal char(5);

  <тело процедуры>

  return retVal;
end-proc;

Здесь описывается процедура myProc, возвращающая строку из 5 символов и двумя параметрами - строка из 5-ти символов и структура, содержащая описание ошибки (если таковая случится) по шаблону t_daPar2. Внутри описана возвращаемая переменная retVal.

Если предполагается, что данная процедура будет вызываться из другого модуля, ее необходимо “экспортировать”:
dcl-proc IncrGIDNumeric export;

Поскольку все имена в RPG регистронезависимые, то эксплуатироваться она будет с именем в верхнем регистре. Если нужно сохранить регистрозависимость имени, то в описании интерфейса добавляется модификатор
dcl-pi *n char(5) extproc(*dclcase);

Параметры в процедуры в RPG по умолчанию передаются по ссылке. Если требуется передача по значению, нужно использовать модификатор value в описании параметра. Это, в том числе, позволяет использовать при вызове процедуры литералы вместо переменных.

Модификатор const означает что в процедуру будет передаваться не сама переменная используемая в вызове, но ее безопасная копия. В этом случае становится возможным использовать литералы вместо переменных в качестве аргументов при вызове процедуры. Т.е.

dcl-proc myProc1;
  dcl-pi *n;
    str char(5);
  end-pi;

  …
  return;
end-proc;

dcl-proc myProc2;
  dcl-pi *n;
    str char(5) const;
  end-pi;

  …
  return;
end-proc;

Вызов myProc1(‘abc’) приведет к ошибке компиляции, а вызов myProc2(‘abc’) скомпилируется и будет работать корректно - в myProc2 мы получим st = ‘abc  ‘

Кроме того для описания параметров есть универсальный модификатор options, в котором указывается одна, или несколько “опций” для данного параметра.

  • options(*varsize) для строк или массивов означает что размер передаваемой переменной может отличаться от размера указанного параметра. Например, если параметр описан как
    str char(65563) options(*varsize);
    означает что при вызове может быть передана строка любого размера. При этом, разработчик должен озаботиться тем или иным образом передать реальный размер строки в процедуру (например, явно передать размер строки отдельным параметром).

  • options(*omit) означает что параметр не является обязательным - вместо него при вызове может быть передано специальное значение *omit (с учетом того, что параметры передаются по ссылке это тождественно передачи nullptr). Естественно, такие параметры перед их использованием требуют дополнительной проверки. Раньше для этого использовалась проверка адреса на null
    if %addr(<имя параметра>) = *null;
    сейчас появилась BIF
    if %omitted(<имя параметра>);

  • options(*nopass) означает что параметр может быть не передан при вызове. Допустимо для последних одного или нескольких параметров (если для какого-то параметра указана эта опция, то она должна быть указана и для всех следующих за ним). Как и в случае с *omit, такие параметры требуют проверки - передан он или нет. Раньше это делалось с помощью BIF %parms (количество параметров, реально переданных при вызове) и %parmnum (номер параметра по его имени)
    if %parms >= %parmnum(<имя параметра>);
    сейчас, наряду с %omitted, появилась %passed
    if %passed(<имя параметра>);

Опции можно комбинировать и использовать совместно. Например:

dcl-proc myProc;
  dcl-pi *n;
    p1 char(5);
    p2 char(5) options(*omit: *nopass);
    p3 char(5) options(*nopass);
  end-pi;

  if %passed(p2) and not %omitted(p2);
    …
  endif;

  if %passed(p3);
    …
  endif;

  return;
end-proc;

1-й параметр обязателен, 2-й и 3-й - нет. Вызывать ее можно

myProc(p1);
myProc(p1: p2: p3);
myProc(p1: *omit: p3);
myProc(p1: p2);

Все варианты будут корректно работать (при условии проверки %passed/%omitted внутри процедуры).

Подпрограммы (subroutines)

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

Тела подпрограмм располагаются между return и end-proc:

…
return;

begsr mySubroutine;
  …
endsr;
end-proc;

Вызов подпрограммы
exsr mySubroutine;

Внутри подпрограммы, при необходимости досрочного выхода из нее, можно использовать оператор  leavesr (аналог leave для выхода из цикла).

Единая точка выхода из процедуры

Еще одна полезная особенность RPG - возможность объявления единой точки выхода из процедуры при помощи блока on-exit

  …
  return;

  on-exit [<индикатор ошибки>];
    …
end-proc;

Управление в этот блок передается в любом случае - нормальный это выход или вылет процедуры по ошибке.

Если требуется диагностировать ситуацию аварийного выхода по ошибке внутри блока on-exit, нужно определить переменную-индикатор и указать ее имя после on-exit

  dcl-s wasError ind;
  …
  return;

  on-exit wasError;
    if wasError;
      …
    endif;
    …
end-proc;

Индикатор автоматически устанавливается в *on в случае ошибки (системного исключения). При нормальном выходе принимает значение *off.

В блоке on-exit допускается изменение возвращаемого значения. Если в нем явного return <value>, то процедура вернет то, что было указано в основном return. Если необходимо вернуть что-то иное, достаточно явно указать return внутри on-exit.

Есть ряд ограничений - например, из on-exit нельзя вызывать подпрограммы и некоторые специфические команды. Но в целом это очень функциональный и удобный механизм для выполнения обязательных  перед завершением процедуры действий (например, освобождение выделенных в процедуре ресурсов).

Прототипы процедур

Как уже говорилось выше, компилятор RPG не является однопроходным и не требует обязательного объявления прототипа процедуры, если она реализована в том же модуле, где вызывается. Но если процедура находится в другом модуле, перед ее вызовом должен быть объявлен ее прототип. прототип объявляется как dcl-pr (Declare Prototype)

dcl-pr <имя процедуры> [<тип возвращаемого значения, если есть>] [<модификаторы>];
  <список аргументов, должен совпадать с тем, что описано в dcl-pi в процедуре>
end-pr;

Одной из особенностей (скорее платформы, нежели языка), является то, что нет большой разницы где находится процедура - в том же модуле, в другом модуле той же программы, в динамической библиотеке (“сервисная программа”) или вообще является отдельной программой. Достаточно правильно описать ее прототип и можно использовать.

Для описания внешних (расположенных вне данного модуля) процедур используются модификаторы

  • extproc(<имя процедуры>) для процедур, расположенных в другом модуле этой же программы или в сервисной программе (раннее связывание). Имя процедуры задается строковым литералом или переменной, содержащей адрес процедуры (pointer(*proc)) что позволяет использовать механизм callback-процедур.

  • extpgm(<имя программы>) для программ, вызываемых как процедуры (позднее связывание). Имя программы передается строковым литералом или указанием имени переменной, содержащей имя программы.

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

Еще один полезный модификатор - opdesc - передача т.н. “операционных дескрипторов” для аргументов процедуры имеющих строковые типы. Операционный дескриптор содержит дополнительную информацию о передаваемом в процедуру объекте. Например, если аргумент описан с модификатором options(*varsize) и передается его операционный дескриптор, внутри процедуры можно (специальным системным API) узнать настоящий размер переданной строки. Но это уже скорее особенность платформы, поддерживаемая языком, нежели самого языка.

Перезагрузка процедур

Относительно недавно в RPG появилась возможность “перезагрузки” процедур близкая к тому, что есть, например, в С++. Этот механизм дает возможность определить единое имя для нескольких процедур, имеющих одинаковый тип возвращаемого значения, но разные наборы аргументов. Реализуется это так:

dcl-pr myProc1 int(10);
  string char(9999) options(*varsize);
  realLen int(10);
end-pr;

dcl-pr myProc2 int(10);
  string varchar(9999) options(*varsize);
end-pr;

dcl-pr myProc int(10) overload(myProc1: myProc2);

И теперь при вызове myProc компилятор сам подставит нужное - если вызывается с двумя аргументами типов char и int(10) - вызовется myProc1, если с одним типа varchar - myProc2.

Работа с БД и коммерческие расчеты

А теперь немного о том, для чего, собственно, предназначен RPG и чем он удобен.

Работа с БД

Поскольку БД (DB2) интегрирована непосредственно в операционную систему, работа с БД на локальной машине сводится к работе с отдельными файлами - “физические файлы” - таблицы, хранилище данных и “логические файлы”, определяющий способ доступа (в простейшем случае - индексы) к одной или нескольким таблицам.

Непосредственная работа с файлами

Исторически основным способом работы с БД была работа непосредственно с физическими или логическими файлами - открыть, установить курсор на нужную запись, прочитать запись, внести изменения, обновить и т.п. 

Чтобы работать с файлом необходимо сначала объявить его как переменную:
dcl-f <имя переменной> <набор модификаторов>;

Например:
dcl-f NMAC4110LF disk(*ext) usage(*input) usropn keyed;

  • disk(*ext) говорит о том, что это дисковый файл - таблица или индекс (бывают еще дисплейные или принтерные файлы, но это уже про ввод/вывод)

  • usage - режим открытия файла. В данном случае только чтение, возможны комбинации из *input, *output, *update и *delete

  • usropn - ручное управление открытием/закрытием. Без этого модификатора файл будет автоматически открываться при появлении в области видимости и автоматически закрываться при выходе из нее

  • keyed - доступ в порядке сортировки ключа, для индексов.

По умолчанию имя файла совпадает с именем переменной. Чтобы использовать отличное от имени переменной имя файла используются модификаторы extfile(*extdesc) extdesc(<имя файла>)

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

Стоит лишь отметить, что по умолчанию компилятор автоматически создает в той же области видимости набор переменных с именами и типами, совпадающими с именами и типами полей в записи, которые будут автоматически заполняться после каждой успешной операции чтения (или их значения использоваться для операции записи). Если это не требуется, используется модификатор qualified и тогда в операциях чтения/записи необходимо указывать буфер - структуру, описанную как likerec со ссылкой на формат записи в данном файле.

Набор команд для работы с файлами минималистичен, но предоставляет все необходимые возможности для работа с данными

  • open/close - открытие/закрытие файла
    open MyFile;

    close MyFile;

  • chain - чтение первой записи с заданным значением ключа. Успешность чтения можно проверить функцией %found
    chain <значение ключа> <файл> [<имя likerec структуры>];
    if %found(<файл>);

    endif;

  • setll/setgt - позиционирование курсора по значению ключа.
    setll позиционирует курсор перед первой записью, значение ключа которой не меньше заданного (если есть запись с равным значением ключа - перед ней, если такой записи нет, то перед записью со следующим в порядка возрастания значением ключа. Функция %equal возвращает *on в случае если курсор установлен перед записью со значением ключа, совпадающим с заданным.
    setgt устанавливает курсор после последней записи со значением ключа не большим (равным или меньшим) заданного. Иными словами - перед первой записью с большим заданного значением ключа.
    Ни та, ни другая функция не читают запись. Только позиционирование.
    Значение ключа может быть не полным - если ключ состоит из нескольких полей (например, 3 поля), для позиционирования можно использовать как все три, так и первое или первое и второе. Например, если есть таблица где содержатся некоторые исторические данные (идентификатор, дата, данные) и есть ключ идентификатор + дата, то для получения записи для идентификатор с минимальной датой позиционирование выглядит так:
    setll ID myFile;
    а для получения записи с максимальной датой так:
    setgt ID myFile;
    Быстро проверить наличие записи для данного идентификатора на данную дату (без чтения данных из таблицы, только наличие в индексе) можно так:
    setll (ID: DT) myFile;
    if %equal(myFile);
    // запись есть
    else;
    // записи нет
    endif;

  • read/readp/reade/readpe - чтение записи.
    read - читает следующую запись
    readp - читает предыдущую запись
    общий синтаксис
    read[p] <файл> [<имя likerec структуры>];
    reade - читает следующую запись с указанным значением ключа
    readpe - читает предыдущую запись с указанным значением ключа
    общий синтаксис
    read[p]e <значение ключа> <файл> [<имя likerec структуры>];
    Если нет подходящей записи (конец файла для read, начало для readp, конец файла или другое значение ключа для reade, начало файла или другое значение ключа для readpe).
    Если взять предыдущий пример с минимальной максимальной датами, то это будет выглядеть так.

    Минимальная дата:
    setll ID myFile;
    reade ID myFile;
    if not %eof(myFile);
    // Прочитали запись минимальной датой для ID
    endif;

    Максимальная дата:
    setgt ID myFile;
    readpe ID myFile;
    if not %eof(myFile);
    // Прочитали запись с максимальной датой для ID
    endif;

  • write - добавление новой записи
    write <файл> [<имя likerec структуры>];

  • update - изменение последней прочитанной записи
    update <файл> [<имя likerec структуры>];

  • delete - удаление последней прочитанной записи или записи для заданного значения ключа
    delete [<значение ключа>] <имя файла>;
    Если не указано значение ключа, удаляется последняя прочитанная запись (т.е. перед delete должно идти или chain, или readxx).
    Если указано значение ключа, то удаляется первая запись для данного значения ключа.

Использование SQL

С некоторого момента в RPG был добавлена возможность использовать встроенные непосредственно в RPG код SQL запросы. Просто пишем exec sql и дальше запрос. 

Если в RPG программе используется SQL, компилируется она специальной командой. Сначала отрабатывает SQL препроцессор, который добавляет нужные структуры данных - SQLDA (SQL Data Area), SQLCA (SQL Communication Area) и заменяет exec sql … на вызовы системных API. 

Например, вместо
exec sql fetch curECLRCLCltAddr for :SQL_ROWS rows INTO :DataBuf;
будет что-то типа такого
SQLER5 = SQL_ROWS;
SQLER6 = -4;
SQCALL000006(SQLCA: SQL_00026: DATABUF);

Тут все основано на том, что БД на этой платформе интегрирована в систему (является ее частью). И SQL движок, также, является частью системы. Т.е. все это происходит на уровне вызова системных API, а не какой-то сторонней библиотеки.

Есть два типа встроенного SQL - динамический и статический. Динамический - когда строка запроса формируется в рантайме, а затем, также в рантайме, строится запрос
exec sql prepare stmtCliTest from :sqlQuery;
exec sql declare CliTest cursor for stmtCliTest;
sqlQuery тут строка запроса, CliTest - курсор, построенный не ее основе. Но это не очень хорошо с точки зрения производительности, особенно, для программ с высокой плотностью вызовов - на подготовку запроса уходит ощутимое время и ресурсы - на формирование SQLCA/SQLDA может уходить до 30% от общего времени работы запроса.

Статический SQL - это когда строка запроса пишется непосредственно в декларации курсора:
exec sql declare ECLRCLModeD1Cur cursor for
              select ECSULST, ECSUID, ECHDLV,
....
или, если курсор не нужен
exec sql select arName, arAdd1, arAdd2, arCity, arStte, arZip
into :outName, :outAddr1, :outAddr2, :outCity, :outState, :outZip
from ARMSTF1
where arCNum = :inCusNo
fetch first 1 row only;

В этом случае основная часть подготовки запроса (формирование SQLDA/SQLCA) происходит на этапе компиляции - в рантайме все это будет выполняться быстрее и потреблять меньше ресурсов.

В целом встроенный SQL позволяет писать достаточно сложные запросы (в т.ч. с подзапросами и обобщенными табличными выражениями).

Очевидное удобство - SQL запрос можно "обкатать" в интерактиве, посмотреть примерный план запроса, рекомендации оптимизатор, а затем уже готовый текст запроса скопировать в RPG код, просто добавив перед ним exec sql.

Также на RPG можно писать хранимые процедуры, UDF/UDTF для SQL. Программы на RPG могут возвращать ResultSet или принимать ResultSet из другой RPG программы.

Однако, нельзя сказать, что он однозначно лучше и быстрее прямого доступа к БД. Практика показывает, что однократное чтение записи (или относительно небольшая выборка по индексу из одной таблицы или по 2-3, но "разбавленная" промежуточной логикой на RPG) с использованием прямого доступа проще и эффективнее нежели SQL. SQL же хорош на сложных запросах со многими условиями по нескольким таблицам. Хотя, тоже из практики, если требуется какое-то агрегирование, то эффективнее его реализовать на уровне RPG кода, чем в SQL запросе. Также SQL не всегда эффективен в тех программах, у которых очень высокая плотность одновременных (параллельных) вызовов из разных мест. Тут тоже прямой доступ может оказаться лучше как минимум в плане потребления ресурсов.

Так или иначе, в RPG всегда можно выбирать как в данном конкретном случае удобнее и эффективнее работать с БД.

Коммерческие вычисления

Как уже было отмечено, RPG поддерживает практически все типы данных, которые есть в БД. За исключением ряда специфических типа CLOB, BLOB, DBCLOB, BINARY, VARBINARY, DATALINK, ROWID, XML, RESULTSET LOCATOR - такие переменные объявляются через sqltype
dcl-s clob sqltype(CLOB: 65536);

Но все, что используется в типовых коммерческих расчетах поддерживается в полном объеме - дата, время, числа с фиксированной точкой (все, финансовые операции производятся только с числами с фиксированной точкой дабы избежать накопления ошибок округления).

Чтобы работать с таблицей, записи которой содержат поля типов date, time, decimal, numeric достаточно просто объявить структуру
dcl-ds dsMyTblRec likerec(MYTBL.MYTBLRECF: *all);
и дальше в нее уже можно читать запись
read MYTBL.MYTBLRECF dsMyTblRec;

Никаких маппингов, никаких дополнительных преобразований, никаких дополнительных объектов создавать не нужно. Если в структуре есть поле типа date - с ним можно работать как с датой - увеличивать/уменьшать на нужное количество дней/месяцев/лет, получить разность между двумя датами в днях/месяцах/годах, представить дату в виде строки или числа в указанном формате (а форматов поддерживается достаточно много) и т.д. и т.п. Аналогичные операции можно проводить с типами time и timestamp. Можно сложить date и time и получить timestamp. Все это заложено непосредственно в язык. Никаких дополнительных зависимостей и лишних действий не требуется.

Если в записи есть поля в формате decimal (packed в RPG) или numeric (zoned в RPG) - это обычные числовые типы с ними можно производить любые действия как с int или float. Сразу после того, как запись прочитана в структуру.

При работе с числами в формате с фиксированной точкой иногда требуется округлять результат при присвоении его переменной. Для этой цели используется команда eval(h) в начале строки (перед операцией). Например, присвоение переменной с двумя знаками после запятой значения с большим количеством знаков после запятой

dcl-s var1 packed(5:2);            // число с фиксированной точкой, 5 знаков, 2 после запятой
dcl-s var2 packed(5:3) inz(2.345); // число с фиксированной точкой, 5 знаков, 3 после запятой, инициализированно значением 2.345

var1 = var2; //последний знак после запятой отбрасывается - var1 = 2.34
eval(h) var1 = var2; // последний знак отбрасывается с округлением - var1 = 2.35

Это будет работать для любых операций с форматами с фиксированной точкой. Тут еще надо иметь ввиду, что при выполнении арифметических операций формат промежуточного результата выбирается компилятором так, чтобы результат туда поместился с максимальной точностью. И только потом уже промежуточный результат будет заносится в lvalue переменную с округлением или без оного.

Если размер lvalue переменной недостаточен для размещения результата (например, переменная задана как 5 знаков, а в результате получили 7), возникнет системное исключение result too large. Такие ситуации необходимо обрабатывать отдельно.

Про арифметику дат и времени уже упоминалось выше. RPG "из коробки" предоставляет полный набор функций для этого - инкремент/декремент на нужное количество единиц, разность между двумя датами/временами, выделение составной части даты/времени, конвертация даты/времени в/из строку или число в заданном формате (*ISO, *EUR и т.п.), проверка строки/числа на то, является ли это корректной датой в заданном формате (может ли быть сконвертировано в дату).

Также есть богатый набор операций со строками - замена символов (символ-в-символ), поиск подстроки, замена подстроки, разбиение строки в массив элементов по заданному разделителю, склейка строки из массива элементов с заданным разделителем... Например, простейший способ удаления лишних пробелов:

dcl-s arr dim(*auto: 100); // динамический массив для элементов
dcl-s str char(100) inz('This     is   just  a   string'); // строка с лишними пробелами

arr = %split(str); // разбиение строки по пробелу (разделитель по умолчанию, можно задавать иное)
str = %concatarr(' ': %subarr(arr: 1: %elem(arr))); // склеиваем обратно

На выходе получим 'This is just a string' в str и ('This', 'is', 'just', 'a', 'string') в arr.

Есть форматирование строки, например, преобразование числа в строку с добавлением ведущих нулей

dcl-s val packed(5:0) inz(3); 
dcl-s str char(5);

str = %char(val); // str = '5';
str = %editc(val: 'X'); // str = '00005'

Есть и более сложные коды форматирования, в основном касающиеся представления различных сумм - с разделением на разряды, добавлением символа валюты и т.п. Или форматирование номеров телефонов... В целом достаточно специфические штуки.

Для работы с массивами (в т.ч. и массивами структур) также есть набор функций - сортировка (для структур может быть сортировка по нескольким полям), поиск по массиву (причем, не только по равенству, но и по больше/меньше, больше-равно/меньше-равно, не равно. Если массив сортированный (ascend/descend в описании), компилятор автоматически будет использовать быстрый двоичный поиск, если нет - просто перебором.

Про структуры данных также написано выше - мощно и гибко для описания достаточно сложных структур. Плюс возможность привязки к форматам записей в БД.

Ошибки, исключения

Структурированная ошибка в IBM i

На данной платформе широко используется т.н. "структурированные ошибки" представляющие из себя структуру, состоящую из 7-значного кода и блока данных (параметров). Данные для таких ошибок хранятся в специальных message-файлах (*MSGF). Там хранится код сообщения, например, KSM0026, его текст: "Period code must be &1 &2 &3" (&1, &2 и &3 - места для подстановки параметров), уровень серьезности ошибки (0 - инорфмацонное, 10 - предупреждение, 20 и выше - серьезная блокирующая ошибка), описание параметров

                                 Десятичн.     Перем.         
Поле     Тип данн.     Длина      позиции      длина      Дамп
&1       *CHAR            10                              *NO 
&2       *CHAR            10                              *NO 
&3       *CHAR            10                              *NO 

Сообщения могут добаляться в файл специальной командой языка CL

INSMSGD MSGID(KSM0026) MSGF(LIB/KSMMSGF) MSG('Period code must be &1 &2 &3') SEV(20) INVERSION(*NO) REPLACE(*YES) LENGTH(10 10 10)

Такие сообщение кодируются структурой

dcl-ds dsError qualified;
  errCode  char(7);
  errParm  char(10) dim(3);
  errData  char(30) samepos(errParm);
end-ds;

Заполняем структуру и возвращаем ошибку. Для получения полного текста ошибки (с подстановкой параметров есть BIF %msg(errCode: msgFile: errData). Также есть системное API, возвращающее более полную информацию об ошибке включая уровень серьезности и т.п.

Исключения

В RPG нет языковых исключений. Скорее всего потому, что язык живет (и процветает) на платформе, где развиты и доступны более универсальные и мощные системные исключения. Инициировать такое исключение можно посылкой сообщения в очередь программы (PGMQ). При этом сообщение автоматически записывается в лог задания (joblog)

Есть разные типы сообщений - информационные (не приводят к исключению, только запись в joblog), прерывающие (собственно исключение), запросы, требующие ответа оператора...

Делается это специальным системным API, но относительно недавно в RPG появилась упрощенная команда для этого - snd-msg которую можно считать аналогом throw.

Аналог try/catch существовал давно и использовался для обработки исключений, генерируемых внешними программами - это monitor (аналог try) и следующие за ним блоки on-error или on-excp (аналог catch).

Разница между on-error и on-excp в том, что первый работает по числовым кодам ошибок, второй - по 7-значным кодам структурированной ошибки. Например

monitor;
  // делаем что-то, что может вызвать исключение
  on-excp 'ABC1234';
    // если возникло исключение с кодом 'ABC1234'

  on-error 00121;
    // код ошибки, возникающий, например при выходе за границы массива

  on-error;
    // все остальные ошибки

endmon;

Как сказано выше, чтобы сгенерировать исключение с заданным кодом, можно воспользоваться snd-msg
snd-msg *escape %msg('ABC1234': 'MYMSGF'[: msgData]);
*escape - прерывающая ошибка, MYMSGF - имя *MSGF файла с ее описанием, msgData - необязательный параметр с данными для подстановки в шаблон.

В простейшем случае выглядеть это будет так:

dcl-proc myProc2;
  dcl-pi *n;
    prm1 char(5);
    prm2 packed(15: 0);
  end-pi;

  //делаем что-то...
  if ... // что-то пошло не так - кидаем исключение
    snd-msg *escape %msg('ABC1234': 'MYMSGF');
  endif;

  return;
end-proc;

dcl-proc myProc1;
  dcl-pi *n;
  end-pi;

  dcl-s prm1 char(5);
  dcl-s prm2 packed(15: 0);

  monitor;
    myProc2(prm1: prm2);
  on-excp 'ABC1234';
    //Обрабатываем выброшенное в myProc2 исключение
  endmon;

  return;
end-proc;

Все необработанные исключения в рамках задания пробрасываются выше по стеку. Если программа А вызвала программу Б и там возникло исключение, которое не было обработано, оно "прилетит" в программу А. И не важно на каких языках написаны А и Б - это универсальный системный механизм.

Как уже говорилось выше, в RPG внешние программы вызываются как обычные процедуры, описанные с модификатором extpgm. Для таких программ-процедур есть еще один способ обработки ошибок - вызов их не напрямую, а через callp(e) (e - "подавление" ошибок - может применяться с любой командой языка для перехвата внутренней ошибки).

dcl-pr myProc extpgm('MYPGM');
...

callp(e) myProc(...);
if %error();
  // случилась ошибка.
endif;

Можно посмотреть числовой код (возвращается функцией %status()) или определить 7-значный код последней ошибки, определив внешнюю переменную
dcl-s  CPFMsgID char(7) import('_EXCP_MSGID');
(примерно как в С определяется __errno).

Аналогично для любой другой команды языка - open(e), write(e) и т.д. и т.п.

Также в процедуре (в т.ч. и главной) можно определить подпрограмму со специальным именем *pssr. Если таковая определена, то в случае необработанного исключения, прежде чем вылететь из процедуры, управление будет передано туда (и там можно что-то сделать, хотя бы создать дамп для последующего анализа)

begsr *pssr;
  dump;
endsr;

Заключение

Получилось, конечно длинно. Но как получилось. Раскрыты ключевые особенности языка RPG. Конечно, много тонкостей осталось за кадром - их, как ив любом другом языке тут хватает (в основном это касается производительности - как эффективнее сделать то или это...

Конечно, платформа IBM i не так распространена как Windows или Linux, но она имеет свою устойчивую нишу, она развивается и RPG развивается вместе с ней. Даже в РФ как минимум три банка (Альфа, Райффайзен и Росбанк) работают на этой платформе. Были еще серверы в РЖД и ПФР, но используются ли сейчас неизвестно. Как минимум три компании (BTC, Cinimex и RMS Lab) предлагают услуги по разработке на RPG под IBM i. В мире разработчиков больше, конечно - есть устойчивое комьюнити (преимущественно англоязычное) - группы на LinkedIn блоги Scott Klement, Nick Litten, Simon Hutchinson (RPGPGM.COM), ресурсы типа Code400, MySampleCode, WisdomJobs, IT-Jungle. Есть теги rpg и rpgle на SO. Для VSCode есть замечательный набор плагинов IBM i Development Pack (отдельное спасибо Liam Barry Allan).

Так что язык, конечно, экзотический, но вполне себе живой. И, может быть, кому-то все это будет интересно хотя бы теоретически.

Комментарии (7)


  1. rutenis
    18.05.2024 16:04
    +1

    Поинтересуюсь: а сейчас подпрограммы - рекурсивные? (Когда появился RPG, аппаратного стека не было, и адрес возврата записывался в фиксированное место в памяти, что приводило к зацикливанию программы при рекурсивных вызовах.)


    1. SpiderEkb Автор
      18.05.2024 16:04
      +1

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

      Надо будет попробовать ради интереса.

      Одну подпрограмму из другой вызвать не проблема. Т.е. какой-то стек точек возврата там есть.

      Можно ли сабрутину саму из себя вызвать - не знаю...


      1. rutenis
        18.05.2024 16:04
        +1

        Не только саму из себя, кстати. Может ещё быть косвенная рекурсия, самый сложный вариант для поиска ошибки, когда не организован полноценный стек вызовов-возвратов.


        1. SpiderEkb Автор
          18.05.2024 16:04
          +1

          Ну в рекурсиях вообще ошибки сложнее искать.

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

          Одно время было модно делать выход из процедуры через сабрутину - такая единая точка выхода которую можно из любого места процедуры вызывать. Сейчас не актуально т.к. появился on-exit и можно просто ставить return где надо, а в точке выхода уже всю необходимую финализацию делать.

          Так что сабрутины сейчас на второй план отшли.


    1. SpiderEkb Автор
      18.05.2024 16:04
      +1

      Так и осталось

      Пробуем

              dcl-s recLevel          int(5);
              dcl-s counter           int(5)  inz(5);
      
              exsr srRecursion;
              return;
            
              begsr srRecursion;
                recLevel += 1;
                dsply ('Recursion Level - ' + %char(recLevel));
                dsply ('Counter         - ' + %char(counter));
                
                if counter > 0;
                  counter -= 1;
                  exsr srRecursion;
                endif;
                
                recLevel -= 1;
                dsply ('Recursion Level - ' + %char(recLevel));
              endsr;

      Не компилируется - *RNF7112 30 EXSR or CASxx in subroutine SRRECUR... calls the same subroutine; the specification is ignored.

      Пытаемся обмануть

              dcl-s recLevel          int(5);
              dcl-s counter           int(5)  inz(5);
      
              exsr srRecCall;
              return;
            
              begsr srRecCall;
                recLevel += 1;
                dsply ('Recursion Level - ' + %char(recLevel));
                
                exsr srRecursion;
      
                recLevel -= 1;
                dsply ('Recursion Level - ' + %char(recLevel));
              endsr;
              
              begsr srRecursion;
                dsply ('Counter         - ' + %char(counter));
                
                if counter > 0;
                  counter -= 1;
                  exsr srRecCall;
                endif;
              endsr;

      Так компилируется, но...

      DSPLY Recursion Level - 1
      DSPLY Counter - 5
      DSPLY Recursion Level - 2
      DSPLY Counter - 4
      DSPLY Recursion Level - 3
      DSPLY Counter - 3
      DSPLY Recursion Level - 4
      DSPLY Counter - 2
      DSPLY Recursion Level - 5
      DSPLY Counter - 1
      DSPLY Recursion Level - 6
      DSPLY Counter - 0
      DSPLY Recursion Level - 6
      DSPLY Recursion Level - 5
      DSPLY Recursion Level - 4
      DSPLY Recursion Level - 3
      DSPLY Recursion Level - 2
      DSPLY Recursion Level - 1
      DSPLY Recursion Level - 0
      DSPLY Recursion Level - -1
      DSPLY Recursion Level - -2
      ...

      И дальше только ENDJOB помогает.


      1. rutenis
        18.05.2024 16:04
        +1

        Спасибо!


        1. SpiderEkb Автор
          18.05.2024 16:04

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