在SAS编程中,我们通常使用%DO语句来循环宏参数的值,从而多次调用并执行具有不同宏参数的宏程序,然而,使用%DO语句通常涉及在循环中使用宏变量的间接引用。DATA步的CALL EXECUTE 提供了一种不使用%DO语句执行宏程序的替代方法,它消除了宏变量的间接引用,使得程序更加简洁易懂。
一、语法
CALL EXECUTE(参数);
CALL EXECUTE中的参数可以是以下三种情形之一:
用引号括起来的字符串。单引号内的参数在程序执行期间解析。在构造DATA步时,双引号内的参数会解析。
DATA步字符变量的名称,其值是要生成的文本表达式或SAS语句。不要将DATA步字符变量的名称括在引号中。
由DATA步解析为宏文本表达式或SAS语句的字符表达式。
二、CALL EXECUTE如何工作
首先我们来了解SAS系统如何处理代码,如下图所示。
SAS代码提交运行后,SAS代码立即进入Input Stack,Input Stack是一个虚拟组件,它会保存代码至系统将其推入下一个名为Word Scanner的组件。在Word Scanner中会进行标记化,它根据某些规则将SAS代码进行组装标记。
在这里,具有特殊字符“&”和“%”的标记可以触发Macro Processor。Macro Processor利用Symbol Table或Macro Catalog中预先存储的信息,通过与Input Stack和Word Scanner的交互来处理宏表达式(宏变量或宏指令)。如果Word Scanner没有发现此类特殊字符,则标记将移动到Compiler,Compiler在收到数据步边界标记(例如RUN、QUIT)后编译代码。
在此编译阶段,Compiler检查代码语法,设置PDV并执行KEEP和DROP等某些语句。编译后的代码会继续移动并最终到达Execution Module,它会指示系统对PDV做更多的事情,以创建新的数据集或修改旧的数据集。
CALL EXECUTE的神奇之处在于,当包含CALL EXECUTE的“父”代码仍在Execution Module中时,它能够从PDV中提取数据值,用它们构建新的“子”SAS代码,然后将新的“子”SAS代码发送回Input Stack进行存储。新构造的代码可以是宏表达式或数据步语句。在Input Stack中,“子”代码以创建的顺序排列。当“父代码”执行完毕后,它们就开始依次沿着这条“装配线”移动。通过这种方式,可以利用driver table中的数据值来生成与数据相关的后续代码,以实现数据驱动的编程设计。
三、参数解析
通常需要引号来区分参数中的“静态”部分(字符串)和“动态”部分(字符表达式),不同的引号会导致CALL EXECUTE以不同的方式解析参数。但在实际编程实践中,如果后续代码仅与数据步语句相关,则两种引号都会给出相同的预期“子”代码。如果后续代码与宏相关,双引号可能会导致意外结果,我们对此进行详细讨论。
1、参数字符串没有宏或宏变量
如果CALL EXECUTE的参数字符串没有宏或宏变量,则参数字符串为在引号内的字符常量和SAS变量的简单连接,SAS变量在每次DATA步迭代时都会被CALL EXECUTE替换为它们的值,并构建SAS“子”代码附加到当前“父”代码所在DATA步后。DATA步完成后,SAS“子”代码将按照创建顺序依次执行。
/*example 1 Argument string has no macro or macro variable reference*/
/* Creating a driver table*/
data tablelist;
cars
class
classfit
;
run;
/*Loading multiple tables*/
data _null_;
set tablelist;
call execute(cat(
'data ',strip(tname),';',
'set sashelp.',strip(tname),';',
'run;'));
run;
在这个例子中,我们使用DATA步循环遍历driver table(tablelist),对于tname列的每个值,都会在DATA步之后生成并执行一个新的数据步。
2、参数字符串在双引号中引用了宏变量
如果CALL EXECUTE的参数具有双引号中的宏变量引用,参数作为字符表达式在“父”代码的编译阶段将被解析。
/*example 2 Argument string has macro variable reference in double quotes*/
%let olib = sashelp;
%let nlib = work;
data _null_;
set tablelist;
call execute(cats(
"data &nlib..",tname,';',
"set &olib..",tname,';',
'run;'));
run;
3、参数字符串在单引号中引用了宏或宏变量
如果CALL EXECUTE的参数有单引号中的宏或宏变量引用,参数作为字符串在“父”代码的编译阶段不会被解析,并在执行阶段以字符串形式构造“子”代码,最终在“子”代码的编译阶段被解析。
/*example 3 Argument string has macro or macro variable reference in single quotes*/
%let olib = sashelp;
%let nlib = work;
data _null_;
set tablelist;
call execute(cats(
'data &nlib..',tname,';',
'set &olib..',tname,';',
'run;'));
run;
4、时间因素
如果宏在运行时分配宏变量,那么在CALL EXECUTE生成代码之前,会先在“父”代码的编译阶段解析这些宏变量,导致warning的出现。
/*example 4 Timing considerations*/
%macro onetable (tname);
proc contents data=sashelp.&tname out=one(keep=name) noprint;
run;
proc sql noprint;
select name into :varlist separated by ' ' from one;
quit;
%put &varlist;
data work.&tname;
retain &varlist;
set sashelp.&tname end=last nobs=n;
if last then call symput('n',strip(put(n,best.)));
run;
%put Table &tname has &n observations.;
%mend onetable;
/*without %nrstr */
data _null_;
set tablelist;
call execute('%onetable('!!strip(tname)!!');');
run;
为了避免过早解析宏变量,在CALL EXECUTE生成代码之前,我们可以在“父”代码的编译阶段抑制宏变量的解析。为此,我们可以使用%nrstr宏函数屏蔽&和%字符。在这种情况下,宏变量将在“子”代码的编译阶段被解析。
/*with %nrstr */
data _null_;
set tablelist;
call execute('%nrstr(%onetable('!!strip(tname)!!'));');
run;
5、CALL EXECUTE参数是SAS变量
CALL EXECUTE的参数可以是一个SAS变量,确切地说是字符变量。在这种情况下,CALL EXECUTE的行为与参数为单引号字符串时的行为相同。这意味着,如果宏或宏变量是参数值的一部分,则需要使用%nrstr宏函数对其进行遮蔽,以避免warning的出现。
/*example 5 CALL EXECUTE argument is a SAS variable*/
arg = '%nrstr(%mymacro(parm1=VAL1,parm2=VAL2))';
call execute(arg);
6、CALL EXECUTE实现完全由数据驱动程序
在上述示例中,我们使用driver table(tablelist)为每个数据步迭代检索单个宏参数的值。然而,我们不仅可以使用driver table动态地为一个或多个宏参数赋值,还可以控制在每个数据步迭代中执行哪个宏。下图说明了完全由数据驱动的SAS程序的过程:
总结
CALL EXECUTE的使用,不仅可以消除在宏程序中使用迭代%DO循环和间接引用,使得程序直白易懂,还可以根据程序驱动表动态生成和运行SAS程序实现一定程度上的自动化。然而,知道何时使用CALL EXECUTE并理解宏编译的机制对于编写准确的宏程序至关重要。
参考文献
https://blogs.sas.com/content/sgf/2017/08/02/call-execute-for-sas-data-driven-programming/
extension://idghocbbahafpfhjnfhpbfbmpegphmmp/assets/pdf/web/viewer.html?file=https%3A%2F%2Fpharmasug.org%2Fproceedings%2F2015%2FBB%2FPharmaSUG-2015-BB15.pdf