В предыдущих статьях (Data-driven SAS macro loops, Modifying variable attributes in all datasets of a SAS library, Automating the loading of multiple database tables into SAS tables) я представил различные приложения, управляемые данными и созданные с использованием макросов SAS.

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

Один из таких инструментов — процедура CALL EXECUTE. Процедура CALL EXECUTE принимает единственный аргумент, который является символьной строкой или символьным выражением. Символьное выражение обычно представляет собой объединение строк, содержащих элементы кода SAS, которые должны быть выполнены после их разрешения. Выражение аргумента может включать символьные константы, переменные шага данных, ссылки на макропеременные, а также ссылки на макросы. Процедура CALL EXECUTE динамически создает код SAS во время итераций шага DATA. Этот код выполняется после завершения шага DATA. Это позволяет превратить шаг DATA, который выполняет итерации в таблице драйверов, в эффективный генератор кода SAS, который аналогичен генератору макросов SAS.

Однако довольно своеобразные правила разрешения аргументов процедуры CALL EXECUTE могут затруднить ее использование. Давайте проясним этот вопрос.

 

Строка аргумента не имеет ссылки на макрос или макропеременную

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

Преимущество этой модели заключается в том, что строка аргумента может представлять собой объединение символьных констант (в одиночных или двойных кавычках) и переменных SAS, которые заменяются их значениями при вызове процедуры CALL EXECUTE для каждой итерации шага DATA. Это приводит к созданию динамически генерируемого кода SAS, точно так же, как в итеративном макроцикле SAS.

Рассмотрим следующий пример. Предположим, что нам нужно загрузить несколько таблиц Oracle в таблицы SAS.

Шаг 1. Создание таблицы драйверов

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

/* create a list of tables to extract & load */
libname parmdl '/sas/data/parmdata';
data parmdl.tablelist;
        length tname $8;
        input tname;
        datalines;
ADDRESS
ACCOUNT
BENEFIT
FINANCE
HOUSING
;

Эта программа запускается только один раз и создает таблицу драйверов parmdl.tablelist.

Шаг 2. Загрузка нескольких таблиц

Затем вы можете использовать следующую программу, управляемую данными, которая запускается каждый раз, когда вам нужно перезагрузить таблицы Oracle в SAS:

/* source ORACLE library */
libname oralib oracle path="xxx" schema="yyy" user="uuu"
 	PASSWORD="{SAS002}ABCDEFG12345678RTUR" access=readonly;
 
/* target SAS library */
libname sasdl '/sas/data/appdata';
 
/* driver table SAS library */
libname parmdl '/sas/data/parmdata';
 
data _null_;
   set parmdl.tablelist;
   call execute(cats(
      'data sasdl.',tname,';',
         'set oralib.',tname,';',
      'run;'));
run;

Чтобы объединить компоненты аргумента CALL EXECUTE, я использовал функцию cats (), которая возвращает объединенную строку без начального и завершающего пробелов.

При запуске этой программы после шага данных _null_ добавляются и выполняются следующие операторы (данные получены из журнала SAS):

NOTE: CALL EXECUTE generated line.
1   + data sasdl.ADDRESS;set oralib.ADDRESS;run;
2   + data sasdl.ACCOUNT;set oralib.ACCOUNT;run;
3   + data sasdl.BENEFIT;set oralib.BENEFIT;run;
4   + data sasdl.FINANCE;set oralib.FINANCE;run;
5   + data sasdl.HOUSING;set oralib.HOUSING;run;

В этом примере мы используем шаг _null_ для обработки списка таблиц (parmdl.tablelist). Для каждого значения столбца tname создается новый шаг данных. Этот шаг данных выполняется после шага данных _null_. Этот процесс показан на следующей схеме:

Diagram explaining CALL EXECUTE for SAS data-driven programming

 

Строка аргумента содержит ссылку на макропеременную в двойных кавычках

Если аргумент CALL EXECUTE содержит ссылки на макропеременные в двойных кавычках, эти ссылки будут разрешены предварительным обработчиком макросов SAS во время компиляции шага DATA. Ничего необычного. Например, следующий код будет выполняться точно так же, как указано выше, и ссылки на макропеременные &olib и &slib будут разрешены в oralib и sasdl соответственно:

%let olib = oralib;
%let slib = sasdl;
 
data _null_;
   set parmdl.tablelist;
   call execute (
      "data &slib.."!!strip(tname)!!';'
         "set &olib.."!!strip(tname)!!';'!!
      'run;'
   );
run;

 

Строка аргумента содержит ссылку на макрос или макропеременную в одинарных кавычках

Здесь начинается интересное. Если аргумент процедуры CALL EXECUTE содержит ссылку на макрос или макропеременную в одинарных кавычках, этот аргумент все равно будет разрешен до того как код будет извлечен из шага DATA. Однако это извлечение выполняется не обработчиком макросов во время компиляции шага DATA, как для двойных кавычек. Ссылки на макросы или макропеременные в одинарных кавычках разрешаются самой процедурой CALL EXECUTE. Например, следующий код будет выполняться точно так же, как указано выше, и ссылки на макропеременные &olib и &slib будут разрешены процедурой CALL EXECUTE:

%let olib = oralib;
%let slib = sasdl;
 
data _null_;
   set parmdl.tablelist;
   call execute('data &slib..'!!strip(tname)!!';'!!
                'set &olib..'!!strip(tname)!!';'!!
                'run;'
               );
run;

 

Соображения, касающиеся синхронизации

ВНИМАНИЕ: Если ваш макрос содержит некоторые конструкции, не связанные с макросом, для присваивания макропеременных во время выполнения — например, CALL SYMPUT или SYMPUTX (на шаге DATA) или предложение INTO (в PROC SQL) — то разрешение ссылок на эти макропеременные процедурой CALL EXECUTE произойдет слишком рано, прежде чем будет извлечен и выполнен сгенерированный макросом код. Это приведет к неразрешенным макропеременным. Давайте запустим следующий код, который извлекает таблицы Oracle в таблицы SAS, как указано выше, а также изменяет положения столбцов по названиям столбцов в алфавитном порядке:

%macro onetable (tblname);
   proc contents data=oralib.&tblname out=one(keep=name) noprint;
   run;
 
   proc sql noprint;
      select name into :varlist separated by ' ' from one;
   quit;
   %put &=varlist;
 
   data sasdl.&tblname;
      retain &varlist;
      set oralib.&tblname end=last nobs=n;
      if last then call symput('n',strip(put(n,best.)));
   run;
   %put Table &tblname has &n observations.;
%mend onetable;
 
data _null_;
   set parmdl.tablelist;
   call execute('%onetable('!!strip(tname)!!');');
run;

Как и ожидалось, в журнале SAS появятся неразрешенные ссылки на макропеременные:

WARNING: Apparent symbolic reference VARLIST not resolved.
WARNING: Apparent symbolic reference N not resolved.
Table ADDRESS has &n observations.

РЕШЕНИЕ: Чтобы избежать проблем с синхронизацией, когда ссылка на макрос разрешается процедурой CALL EXECUTE слишком рано, до того как будут присвоены во время исполнения шагов макросов, мы можем удалить CALL EXECUTE из привилегии разрешения макроса. Чтобы это сделать, можно замаскировать символы & и % с помощью функции макроса %nrstr. При этом процедура CALL EXECUTE "перестает видеть макросы", поэтому она извлечет код макроса без разрешения этого кода. В этом случае разрешение макроса произойдет после шага DATA, в котором находится процедура CALL EXECUTE. Если аргумент CALL EXECUTE вызывает макрос, то можно включить его в функцию макроса %nrstr. Следующий код будет работать корректно:

data _null_;
   set parmdl.tablelist;
   call execute('%nrstr(%onetable('!!strip(tname)!!'));');
run;

При выполнении этого шага DATA добавляются и выполняются следующие операторы (данные получены из журнала SAS):

NOTE: CALL EXECUTE generated line.
1   + %onetable(ADDRESS);
2   + %onetable(ACCOUNT);
3   + %onetable(BENEFIT);
4   + %onetable(FINANCE);
5   + %onetable(HOUSING);

Параметр CALL EXECUTE — переменная SAS

Аргумент процедуры CALL EXECUTE необязательно должен быть символьной константой или включать символьную константу. Он может представлять собой переменную SAS, точнее, символьную переменную. В этом случае поведение процедуры CALL EXECUTE аналогично поведению для аргументов в одинарных кавычках. Это означает, что если ссылка на макрос является частью значения аргумента, ее нужно замаскировать, используя макрофункцию %nrstr(), чтобы избежать упомянутой выше проблемы с синхронизацией.

В этом случае аргумент CALL EXECUTE может выглядеть следующим образом:

arg = '%nrstr(%mymacro(parm1=VAL1,parm2=VAL2))';
call execute(arg);

Делаем процедуру CALL EXECUTE полностью управляемой данными

В приведенных выше примерах мы использовали драйвер таблиц tablelist для получения значений одного параметра макроса для каждой итерации шага данных. Однако таблицу драйверов можно использовать не только для динамического присвоения значений одному или нескольким параметрам макроса, но и для управления тем, какой макрос будет выполняться на каждой итерации шага данных. На следующей схеме показана программа SAS, полностью управляемая данными:

Diagram explaining using CALL EXECUTE for SAS data-driven programming

 

Заключение

CALL EXECUTE — мощный инструмент для разработки приложений SAS, управляемых данными. Надеюсь, из этой статьи вы поняли, как избежать недостатков этого инструмента и эффективно использовать его для своих целей. Я буду рад вашим комментариям и хотел бы услышать ваши впечатления от использования процедуры CALL EXECUTE.

Оригинал статьи на английском