0
votes

To refactor a program, I took a complex process I want to abstract and placed it within a macro.

%macro BlackBox();
  data _null_;
    put "This represents a complex process I want to abstract.";
  run;
%mend;

The process needs to occur multiple times in succession, so the obvious solution is to place it within a loop.

data _null_;
  do i = 1 to 3;
    %BlackBox();
  end;
run;

This, however, produces the following error.

ERROR 117-185: There was 1 unclosed DO block.

What is happening?

My best guess is that SAS is trying to run a data step within a data step.

I find that I can avoid this error by enclosing my loop within a macro and then immediately calling the macro.

%macro PerformDoLoop();
  %do i = 1 %to 3;
    %BlackBox();
  %end;
%mend;
%PerformDoLoop;

All this seems like an roundabout way of handling a fundamental programming task. I'm hoping that learning more about why the data step approach fails will give me insight into how to complete this task more elegantly.

Please understand that this is a simplified example used to illustrate the error I am encountering. A real instance of the macro may take in arguments or return values.

2

2 Answers

2
votes

Your assumption is precisely correct; SAS is trying to execute a data step within a data step, and that's not going to go anywhere of course (well, it's possible, but only ... complexly).

The macro loop method is entirely reasonable, and I would argue for other programming languages that's basically what you'd do. You write a method draw_box to display a box on the screen in C#, then you write a method draw_three_boxes that displays three boxes on the screen by calling draw_box three times.

Now, the reason it seems silly is that you have bad programming design insomuch as the draw_three_boxes method is very limited: it's only capable of doing one thing, drawing three boxes, so why didn't you just make the original draw_box method do that in the first place?

Presumably, what you should want to do is write draw_box and then write draw_boxes(int count, int xpos, int ypos) or something like that, right? Same thing here. You shouldn't write the PerformDoLoop() macro as you did because you're hardcoding the number of times to perform the loop.

Instead get at why you are running it three times. If it's really something you just know and isn't a piece of data, well, write %PerformDoLoop(count=) and then call %PerformDoLoop(count=3). Or include the %do loop in the original macro, with a parameter for count, default it to one.

More likely, there is a data driven reason for doing it 3 times. You have 3 states. You have 3 classes of students. Whatever. Use that to generate the calls to %BlackBox. That will give you the best results, because then you aren't doing it in the program - your data changes, you instantly get 2 or 4 or whatever calls.

You can see my recently presented paper, Writing Code With Your Data from SESUG 2016 for more information on how to do that.

1
votes

The macro language is a pre-processor. It generates SAS code, and it executes before the DATA step code is even compiled. With your code:

data _null_;
  do i = 1 to 3;
    %BlackBox();
  end;
run;

The macro %BlackBox() will execute once (not three times, because it executes before the DO loop executes, conceptually outside of the DO loop). And the data step code becomes:

data _null_;
  do i = 1 to 3;
    data _null_;
    put "This represents a complex process I want to abstract.";
    run;
  end;
run;

As you say, it is not possible in SAS to execute a data step inside another data step. The data _null_ on line 3 ends the first data step, leaving it within an unclosed do block.

I agree with @Joe's points. If you want to generate a number of macro calls, using a macro %DO loop to do so is a fine approach. His paper gives a nice approach for using data to generate macro calls, by building macro variables that resolve to a list of macro calls.

Another useful approach to learn is CALL EXECUTE. This allows you to use a data step to generate macro calls. CALL EXECUTE generates the macro calls when the data step executes, and the macros will execute outside of the data step (when you use %NRSTR as below). For example:

data _null_;
  do i = 1 to 3;
    call execute ('%nrstr(%BlackBox())');
  end;
run;

Will generate three macro calls:

NOTE: CALL EXECUTE generated line.
1   + %BlackBox()
MPRINT(BLACKBOX):   data _null_;
MPRINT(BLACKBOX):   put "This represents a complex process I want to abstract.";
MPRINT(BLACKBOX):   run;

This represents a complex process I want to abstract.

2   + %BlackBox()
MPRINT(BLACKBOX):   data _null_;
MPRINT(BLACKBOX):   put "This represents a complex process I want to abstract.";
MPRINT(BLACKBOX):   run;

This represents a complex process I want to abstract.

3   + %BlackBox()
MPRINT(BLACKBOX):   data _null_;
MPRINT(BLACKBOX):   put "This represents a complex process I want to abstract.";
MPRINT(BLACKBOX):   run;

This represents a complex process I want to abstract.