14

I would like to return a value from a SAS macro I created but I'm not sure how. The macro computes the number of observations in a dataset. I want the number of observations to be returned.

%macro nobs(library_name, table_name);
  proc sql noprint;
    select nlobs into :nobs
    from dictionary.tables
    where libname = UPCASE(&library_name)
      and memname = UPCASE(&table_name);
  quit;

  *return nobs macro variable;
  &nobs
%mend;

%let num_of_observations = %nobs('work', 'patients');

Also, I would like the &nobs macro variable that is used within the macro to be local to that macro and not global. How can I do that?

andrey_sz
  • 751
  • 1
  • 13
  • 29
bambi
  • 364
  • 1
  • 3
  • 12
  • I like Jack Hamilton's macro function for counting obs: http://www2.sas.com/proceedings/sugi26/p095-26.pdf – Quentin Nov 20 '15 at 02:23
  • Thanks for the link. My main concern here is how to return a value from a macro. I'd also really like to know how to make the "into" variable (in a proc sql) local and not global. These are two things I need to do continuously with other macros. – bambi Nov 20 '15 at 03:30
  • Are you sure you need to know how many? Usually I just want to know none or not-none. What will you do with NOBS? – data _null_ Nov 20 '15 at 11:02
  • You can make the _into: variable_ global or local by declaring it inside your macro as such `%global myGlobalMacroVar; %local myLocalMacroVar; ` Do that before using it. – Dirk Horsten Nov 20 '15 at 11:46
  • Agree with @data_null_. Often what you really want is none or not none, which is much faster than counting usually. Jack's paper has an approach for that as well. – Quentin Nov 20 '15 at 14:28
  • @data_null_, I need to know the number of observations for writing data validation test cases. – bambi Nov 20 '15 at 15:02
  • @bambi what are "data validation test cases"? – data _null_ Nov 20 '15 at 18:25

5 Answers5

16

I'll answer the core question Bambi asked in comments:

My main concern here is how to return a value from a macro.

I'm going to quibble with Dirk here in an important way. He says:

A SAS macro inserts code. It can never return a value, though in some cases you can mimic functions

I disagree. A SAS macro returns text that is inserted into the processing stream. Returns is absolutely an appropriate term for that. And when the text happens to be a single numeric, then it's fine to say that it returns a value.

However, the macro can only return a single value if it only has macro statements in addition to that value. Meaning, every line has to start with a %. Anything that doesn't start with % is going to be returned (and some things that do start with % might also be returned).

So the important question is, How do I return only a value from a macro.


In some cases, like this one, it's entirely possible with only macro code. In fact, in many cases this is technically possible - although in many cases it's more work than you should do.

Jack Hamilton's linked paper includes an example that's appropriate here. He dismisses this example, but that's largely because his paper is about counting observations in cases where NOBS is wrong - either with a WHERE clause, or in certain other cases where datasets have been modified without the NOBS metadata being updated.

In your case, you seem perfectly happy to trust NOBS - so this example will do.

A macro that returns a value must have exactly one statement that either is not a macro syntax statement, or is a macro syntax statement that returns a value into the processing stream. %sysfunc is an example of a statement that does so. Things like %let, %put, %if, etc. are syntax statements that don't return anything (by themselves); so you can have as many of those as you want.

You also have to have one statement that puts a value in the processing stream: otherwise you won't get anything out of your macro at all.

Here is a stripped down version of Jack's macro at the end of page 3, simplified to remove the nlobsf that he is showing is wrong:

 %macro check;

   %let dsid = %sysfunc(open(sashelp.class, IS));
   %if &DSID = 0 %then
   %put %sysfunc(sysmsg());

   %let nlobs = %sysfunc(attrn(&dsid, NLOBS));

   %put &nlobs;

   %let rc = %sysfunc(close(&dsid));

 %mend;

That macro is not a function style macro. It doesn't return anything to the processing stream! It's useful for looking at the log, but not useful for giving you a value you can program with. However, it's a good start for a function style macro, because what you really want is that &nlobs, right?

 %macro check;

   %let dsid = %sysfunc(open(sashelp.class, IS));
   %if &DSID = 0 %then
   %put %sysfunc(sysmsg());

   %let nlobs = %sysfunc(attrn(&dsid, NLOBS));

   &nlobs

   %let rc = %sysfunc(close(&dsid));

 %mend;

Now this is a function style macro: it has one statement that is not a macro syntax statement, &nlobs. on a plain line all by itself.

It's actually more than you need by one statement; remember how I said that %sysfunc returns a value to the processing stream? You could remove the %let part of that statement, leaving you with

 %sysfunc(attrn(&dsid, NLOBS))

And then the value will be placed directly in the processing stream itself - allowing you to use it directly. Of course, it isn't as easy to debug if something goes wrong, but I'm sure you can work around that if you need to. Also note the absence of a semi-colon at the end of the statement - this is because semicolons aren't required for macro functions to execute, and we don't want to return any extraneous semicolons.

Let's be well behaved and add a few %locals to get this nice and safe, and make the name of the dataset a parameter, because nature abhors a macro without parameters:

 %macro check(dsetname=);

   %local dsid nlobs rc;

   %let dsid = %sysfunc(open(&dsetname., IS));
   %if &DSID = 0 %then
   %put %sysfunc(sysmsg());

   %let nlobs = %sysfunc(attrn(&dsid, NLOBS));

   &nlobs

   %let rc = %sysfunc(close(&dsid));

 %mend;

 %let classobs= %check(dsetname=sashelp.class);

 %put &=classobs;

There you have it: a function style macro that uses the nlobs function to find out how many rows are in any particular dataset.

Robert Penridge
  • 8,424
  • 2
  • 34
  • 55
Joe
  • 62,789
  • 6
  • 49
  • 67
  • 3
    Gosh, @Joe, I came to write an answer and found you had already written it, so I'll have to wait for next time. That said, I would remove the semicolon that your macro is returning: `&nlobs;` . Typically a macro function returns just the value, without a semicolon, so that it can be called in the middle of a statement. I'm a big fan of Peter Crawford's simple macro utility %NOW, and his macro-function design paper that describes it: http://www2.sas.com/proceedings/sugi31/038-31.pdf. Understanding macro functions changed my understanding of the macro language. – Quentin Nov 20 '15 at 16:05
  • @Quentin Good point, that shouldn't have a `;` in it - fixed. – Joe Nov 20 '15 at 16:06
  • OK, Joe, if you see it that way, ALL macro's are functions. It is all or none. – Dirk Horsten Nov 20 '15 at 19:08
  • @DirkHorsten Oh, none certainly. There is no such thing as a macro function, at least using "correct" terminology. The terminology is "function-style macro". I would say that all macros "return" something, as long as "return" is understood in this context, and _where_ it's returned to. – Joe Nov 20 '15 at 20:22
  • 1
    So, _under the hood_, a macro is a function returning text, which the SASsystem insert in the code before compiling it. The duck-type of a macro can be both function or subroutine – Dirk Horsten Nov 21 '15 at 08:37
  • @Joe There's still a few extra `;`s you missed. Looks like you updated some of them based on Quentin's comment but the ones that still exist are: 1) the return value in the 2nd code block, 2) in the code block with the `%sysfunc()` snippet, in this context I'd consider removing the final semicolon as well. – Robert Penridge Nov 24 '15 at 00:47
  • 1
    @Joe I took the liberty of fixing them myself. – Robert Penridge Nov 24 '15 at 22:11
  • 1
    If you want to use comments in the macro function. Do not use "* xxx;", use "%* xxx;" instead. – Feng Jiang Feb 17 '17 at 17:19
11

What is the Problem writing function-like macros?

i.e. macros you can use as%let myVar = %myMacro(myArgument)

  • You can use your user written macro as if it were a function if all you do is
    • calling some %doSomething(withSometing) like macro functions
    • assign values to macro variables with a %let someVar = statement
    • "return" your result, typically by writing &myResult. on the last line before your %mend
  • As soon as you include a proc or data step in your macro, this does not work any more
  • Luckily, %sysFunc() comes to the rescue, so we can use any data step function
  • This includes low level functions like open, fetch and close which can even access your data
  • nerdy people can do quite a lot with it, but even if you are nerdy, your boss will seldom give you the time to do so.

How do we solve this?, i.e. which building blocks do I use to solve this?

  • proc fcmp allows packaging some data step statements in a subroutine or function
  • This function, meant for use in a data step, can be used within %sysfunc()
  • Within this function you can call run_macro to execute any macro IN BACKGROUND IMMEDIATELY

Now we are ready for the practical solution

Step 1: write a helper macro

  • with no parameters,
  • using some global macro variables
  • "returning" its result in a global macro variable

I know that is bad coding habit, but to mitigate the risk, we qualify those variables with a prefix. Applied to the example in the question

** macro nobsHelper retrieves the number of observations in a dataset
    Uses global macro variables:
        nobsHelper_lib: the library in which the dataset resides, enclosed in quotes
        nobsHelper_mem: the name of the dataset, enclosed in quotes
    Writes global macro variable:
        nobsHelper_obs: the number of observations in the dataset 

    Take care nobsHelper exists before calling this macro, or it will be ost
**;
%macro nobsHelper();
    ** Make sure nobsHelper_obs is a global macro variable**;
    %global nobsHelper_obs;

    proc sql noprint;
        select nobs
        into :nobsHelper_obs
        from sashelp.vtable
        where libname = %UPCASE(&nobsHelper_lib)
          and memname = %UPCASE(&nobsHelper_mem);
    quit;
    %* uncomment these put statements to debug **;
    %*put NOTE: inside nobsHelper, the following macro variables are known;
    %*put _user_;
%mend;

Step 2: write a helper function;

**Functions need to be stored in a compilation library;
options cmplib=sasuser.funcs;

** function nobsHelper, retrieves the number of observations in a dataset
    Writes global macro variables:
        nobsHelper_lib: the library in which the dataset resides, enclosed in quotes
        nobsHelper_mem: the name of the dataset, enclosed in quotes
    Calls the macro nobsHelper

    Uses macro variable:
        nobsHelper_obs: the number of observations in the dataset 
**;
proc fcmp outlib=sasuser.funcs.trial;
    ** Define the function and specity it should be called with two character vriables **;
    function nobsHelper(nobsHelper_lib $, nobsHelper_mem $);
        ** Call the macro and pass the variables as global macro variables 
        ** The macro variables will be magically qouted **;
        rc = run_macro('nobsHelper', nobsHelper_lib, nobsHelper_mem);
        if rc then put 'ERROR: calling nobsHelper gave ' rc=;

        ** Retreive the result and pass it on **;
        return (symget('nobsHelper_obs'));
    endsub;
quit;

Step 3: write a convenience macro to use the helpers;

** macro nobs retrieves the number of observations in a dataset
    Parameters:
        library_name: the library in which the dataset resides
        member_name: the name of the dataset
    Inserts in your code:
        the number of observations in the dataset 
    Use as a function
**;
%macro nobs(library_name, member_name);
    %sysfunc(nobsHelper(&library_name, &member_name));

    %* Uncomment this to debug **;
    %*put _user_;
%mend;

Finally use it;

%let num_carrs = %nobs(sasHelp, cars);
%put There are &num_carrs cars in sasHelp.Cars;

Data aboutClass;
    libname = 'SASHELP';
    memname = 'CLASS';
    numerOfStudents = %nobs(sasHelp, class);
run;

I know this is complex but at least all the nerdy work is done. You can copy, paste and modify this in a time your boss will accept. ;

Dirk Horsten
  • 3,753
  • 4
  • 20
  • 37
  • 1
    Ah, the FCMP/RUN_MACRO solution - was going to write this separately, good to see you've already done it. Look forward to the comments. – Joe Nov 20 '15 at 16:06
  • 1
    Yes, this a nice approach, what Mike Rhoads called the "Macro Function Sandwich" http://support.sas.com/resources/papers/proceedings12/004-2012.pdf. So now perhaps someone will add the similar DOSUBL solution, e.g. https://support.sas.com/resources/papers/proceedings13/032-2013.pdf. I've got reservations about DOSUBL due to possibility of scope collisions, but my goodness it looks really nice. So hopefully it will be refined in future releases. – Quentin Nov 20 '15 at 16:12
  • Thanks for the challenge, Bambie! Joe, Quentin and I enjoyed it very much! – Dirk Horsten Nov 20 '15 at 19:03
  • 1
    You have a typo - `proc fcomp` should be `proc fcmp`. – Robert Penridge Nov 24 '15 at 00:53
3

A SAS macro inserts code. It can never return a value, though in some cases you can mimic functions, usually you need a work around like

%nobs(work, patients, toReturn=num_of_observations )

** To help you understand what happens, I advice printing the code inserted by the macro in your log: ;

options mprint;

We pass the name of the macro variable to fill in to the macro, I find it most practical to

  • not require the user of my macro to put quotes around the libary and member names
  • make the name of the variable a named macro variable, so we can give it a default;

    %macro nobs(library_name, table_name, toReturn=nobs);

Make sure the variable to return exists

  • If it exists it is known outside of this macro.
  • Otherwisse if we create it here, it wil by default be local and lost when we leave the macro;

    %if not %symexist(&toReturn.) %then %global &toReturn.;
    

In the SQL, I

  • use the SASHELP.VTABLE, a view provided by SAS on its meta data
  • add the quotes I omitted in the macro call ("", not '': macro variables are not substituted in single qoutes)
  • use the macro %upcase function instead of the SAS upcase function, as it sometimes improves performance;

    proc sql noprint;
        select nobs
        into :&toReturn.
        from sashelp.vtable
        where libname = %UPCASE("&library_name.")
        and memname = %UPCASE("&table_name.");
    quit;
    

    %mend;

Pay attention if you call a macro within a macro, Run this code and read the log to understand why;

%macro test_nobs();
    %nobs(sashelp, class); ** will return the results in nobs **;

    %nobs(sashelp, shoes, toReturn=num_of_shoes);

    %let num_of_cars = ;
    %nobs(sashelp, cars, toReturn=num_of_cars);

    %put NOTE: inside test_nobs, the following macro variables are known;
    %put _user_;
%mend;

%test_nobs;
%put NOTE: outside test_nobs, the following macro variables are known;
%put _user_;
Dirk Horsten
  • 3,753
  • 4
  • 20
  • 37
  • Could the one who voted me down please explain. Does it not work for you? – Dirk Horsten Nov 20 '15 at 10:57
  • 'Twas me and i apologize for lack of comment. The revision adds helpful content. But the first two sentences are factually incorrect. A macro can return a value, so %let num_of_observations = %nobs('work', 'patients'); can work. This is the benefit of function-style macros such as presented in Jack Hamilton's paper which has a function-style macro to count observations. Thus to me the answer is still wrong. – Quentin Nov 20 '15 at 12:33
  • Thanks for pointing me to Hamilton's example, Quentin. However, it still just insert characters in the pre-compiled code. It solves the question bambi gave as an example, but not the fundamental one. – Dirk Horsten Nov 20 '15 at 14:20
  • I disagree. I think a function-style macro answers the question of how (at least one way) to return a value from a macro. Your way is a different way. Both are useful approaches in different settings. I will try to add a general answer about the function-style macro approach, either today or tomorrow. – Quentin Nov 20 '15 at 14:25
  • Let's agree to disagree. A philosophical discussion about the definition of what a function is does not help bambi. (Isn't there a forum somewhere on this site?) – Dirk Horsten Nov 20 '15 at 14:29
  • This was very helpful @DirkHorsten. Why do you recommend using sashelp.vtable instead of dictionary.tables? What is the advantage? Also, shouldn't I be reading nlobs, rather than nobs, to account for deleted records? Thanks! – bambi Nov 20 '15 at 14:49
  • @bambi The two are identical for all intents and purposes; `sashelp.vtable` is accessible in the data step, `dictionary.tables` is not. – Joe Nov 20 '15 at 15:23
  • Indeed; I used what I know. (If the only tool you know is a hammer, you see everything as a nail.) – Dirk Horsten Nov 20 '15 at 15:41
  • One other question @DirkHorsten: why do you have periods after the macro variable name? i.e. %UPCASE("&library_name."). I tried it without periods ("&library_name") and it works just fine. Just wondering what purpose they serve? Thanks! – bambi Nov 20 '15 at 16:06
  • @bambi, use of periods after all macro var references is mostly a style question. If you have a macro var named MVAR which resolves to Hi and you want to generate the string HiMom (no space), you can do that with &mvar.Mom. In that case the dot is required to end the macro var reference so SAS looks for a macro var named MVAR not one named MVARMOM. Some folks make it a style point to end all macro var references with a dot. – Quentin Nov 20 '15 at 21:12
  • @Quentin I think 'factually incorrect' is open to interpretation here, and a downvote seems pretty harsh. I'm siding with Dirk on this one - the macro pre-processor returns code, not a value. If that code simply happens to be a number, or a string literal, than that is still just code that represents a value. Having said that, the solution in this answer is my least preferred as it lacks the flexibility of a function-style macro etc.. but it is still an acceptable solution. – Robert Penridge Nov 24 '15 at 01:05
  • Fair point @Robert. I re-read the down vote criteria. I see the original answer as "clearly incorrect" and thus worthy of a downvote, as first two sentences were "A SAS macro can never return a value, it inserts code. Therefore, you can never make %let num_of_observations = %nobs('work', 'patients') work." As edited, it is more debatable how correct it is (not to resume that debate. :) And certainly there are many well-made points. I have removed the downvote. – Quentin Nov 24 '15 at 19:15
2

You can't 'return' a value from a function-style macro unless you have written it using only macro statements. Quentin's link provides an example of how to do this.

For example, you cannot use your macro like so, because proc sql cannot execute in the middle of a %put statement (this is possible with other more complex workarounds, e.g. dosubl, but not the way you've written it).

%put %nobs(mylib,mydata);

The best you can do without significant changes is to create a global macro variable and use that in subsequent statements.

To create a macro variable that is local to the originating macro, you have to first declare it via a %local statement within the macro definition.

user667489
  • 9,501
  • 2
  • 24
  • 35
0

I know I am very late to this discussion, but thought of commenting since I came across this. This is another way of doing this I think:

%macro get_something_back(input1, input2, output);
  &output = &input1 + &input2;
%mend;

data _test_;
  invar1 = 1; invar2 = 2;
  %get_something_back(invar1, invar2, outvar);
end;

This will also work outside a datastep.


%global sum;

%macro get_something_back(input1, input2, outvar);
  %let &outvar = &sysevalf(&input1 + &input2);
%mend;

%get_something(1, 2, sum);

SAS2Python
  • 1,277
  • 5
  • 16
  • It does not make any sense to call your macro inside of a data step. It is just generating macro code so it will be completely done while SAS is reading and compiling the code for the data step around it. Just move the macro call to before the data step so it is clearer to the humans reading the code the order they will execute. – Tom Jul 02 '19 at 20:56
  • Having the caller tell the macro which macro variable to create is a good solution to this problem. You should have the macro test whether the target macro variable exists or not. It can then create it as global so that the value doesn't disappear as soon as the macro finishes. `%if not %symexist(&outvar) %then %global &outvar;` – Tom Jul 02 '19 at 20:58