Хитрые функции в SAS Base/Macro

Этот пост я пишу скорее даже для себя. С другой стороны, подобных материалов по SAS Base на русском исчезающе мало, так что может и пригодится кому-то ещё.

Сегодня решая забавную задачку по работе, понял, что мне не хватает произвольной, собственной функции. Как её можно создать в SAS? Ну для начала там есть макросы. Но это не то. Макросы в SAS - это скорее метапрограммирование: ты пишешь код, который потом при запуске сгенерирует обычный SAS Base-код и уже он пойдёт на исполнение. А мне нужно было (не вдаваясь в подробности) формировать из одной таблицы другую по шаблону третьей. Уверен, это можно было реализовать вкраплением SQL-запроса (SAS это тоже позволяет делать), но моя мысль заключалась несколько в другом: как бы так объявить обычную функцию, которая бы преобразовывала дающиеся на вход данные, да ещё с участием данных из других таблиц?

Язык SAS Base/Macro - весьма своеобразная датаориентированная среда, и то, что в языках типа Java или PHP решается на раз, в SAS требует принципиально другого подхода. Поэтому пришлось попотеть. Схематичный исходник и пояснения - под катом:
/* === Начальные данные === */

data users; /* Таблица с юзерами, отсюда потом будем брать имена */
infile datalines;
input id :8. name :$100.;
return;
datalines;
1 Anton
2 Boris
3 Catarina
4 Dmitry
5 Ermak
;
run;

data base(drop=i j); /* Таблица-шаблон. По ней потом будем создавать табличку с именами. Эту таблицу вообще случайными данными забиваем */
array id{6} 8;
do j=1 to 10 by 1;
do i=1 to 6 by 1;
id{i} = floor(6*rand("Uniform")); /* от 0 до 5, 0 - чтобы некоторые поля были пустые на выходе*/
end;
output;
end;
run;

/* === Основная магия тут === */

%macro macro_GetNameByID(); /* Макрос для чтения нужной строки из таблицы USERS */
data _null_;
set users(where=(id=&id));
call symputx('name',name);
%let name=&name;
run;
%mend get_Name_by_ID;

proc fcmp outlib=work.funcs.my;
/* Собственно, сама функция */
function get_name_by_id(id) $; /* $ как признак того, что функция возвращает именно строку, а не число */
length name $100;
rc=run_macro('macro_GetNameByID',id,name); /* Функция вызывает макрос. Макрос, приём! */
return(name);
endsub;
run;

options cmplib=work.funcs;

/* --- Конец магического блока --- */

/* === Основная работа: берём таблицу BASE, и в соотвествии с числами в ней создаём таблицу NAMES === */
data names(drop=i j id:);
set base;
array name{6} $ 100;
array id{6} 8;
do i=1 to 6 by 1;
name{i} = get_name_by_id(id{i});
end;
run;


Что получается? Примерно вот такое непотребство:

WORK.USERS

#idname
11Anton
22Boris
33Catarina
44Dmitry
55Ermak

WORK.BASE

#id1id2id3id4id5id6
1433004
2553010
3031104
4404014
5554530
6514404
7202101
8141033
9430522
10210414

WORK.NAMES

#name1name2name3name4name5name6
1DmitryCatarinaCatarina    Dmitry
2ErmakErmakCatarina  Anton  
3  CatarinaAntonAnton  Dmitry
4Dmitry  Dmitry  AntonDmitry
5ErmakErmakDmitryErmakCatarina  
6ErmakAntonDmitryDmitry  Dmitry
7Boris  BorisAnton  Anton
8AntonDmitryAnton  CatarinaCatarina
9DmitryCatarina  ErmakBorisBoris
10BorisAnton  DmitryAntonDmitry