Pull to refresh
0
InterSystems
InterSystems IRIS: СУБД, ESB, BI, Healthcare

В поисках аналога функций первого порядка в СУБД Caché

Reading time 20 min
Views 3.5K
Пост написан в дополнение к статье Декларативная разработка на Caché.

[2, 3, 5, 7, 11, 13, 17].forEach(function(i) {
  console.log(i);
});

Как делать такое в Caché с помощью COS?
Под катом несколько упражнений на заданную тему.

Чтобы увидеть, как средствами COS — одного из серверных языков, встроенных в Caché — можно добиться такого же лаконичного, наглядного и гибкого кода, был разработан собственный класс test.ForEach для работы с коллекциями-списками.
Исходный код класса test.ForEach
/// Класс-итератор для коллекций.
Class test.ForEach Extends %RegisteredObject 
Final ]
{
/// Коллекция-список <s>почти</s> любого типа.
Property 
collection As %Collection.AbstractList InternalPrivateReadOnlyTransient ];
/// Инициализация свойства <property>collection</property>.
/// <br><br> Допустимые аргументы <var>val</var>:
/// <br> <li>объект класса-наследника от %Collection.AbstractList;
/// <br> <li>список простых элементов в формате $List;
/// <br> <li>список простых элементов в формате строки. В этом случае <var>sep</var> - разделитель элементов в строке;
Method 
%OnNew(valsep ","As %Status PrivateServerOnly = 1 ]
{
    
if $IsObject(val{
        
quit:'val.%Extends("%Collection.AbstractList"$$$ERROR($$$OrefInvalid,val)
        
set i%collection=val
    
}else{
        
set i%collection=##class(%ListOfDataTypes).%New()
        
do ..collection.InsertList($select($listvalid(val):val,1:$listfromstring(val,sep)))
    
}
    
quit $$$OK
}
/// Основной метод-обработчик.
/// <br>
/// <br>Аргументы:
/// <br>
/// <br><var>func</var>:<ol><li>имя метода экземпляра класса</li><li>имя метода класса (любого)</li><li>код в формате команды <link href=/DocBook.UI.Page.cls?KEY=RCOS_cxecute>xecute</link></li><li>некоторые сокращённые команды</li></ol>
/// Примеры вызова:<example>
/// s obj=##class(test.ForEach).%New("2,3,5")
/// ; для каждого элемента коллекции будет вызван соответствующий метод класса с передачей аргументов.
/// ; Первый аргумент выходной/входной, остальные - входные, но это лишь способ соглашения.
/// ; При желании можно поменять их местами, сделать несколько выходных и т.д.
/// d obj.Do("className:methodName",.result,param1,param2,paramN)
/// ; сумма элементов (имеет смысл лишь для коллекции чисел)
/// d obj.Do("+",.result)
/// ; произведение (имеет смысл лишь для коллекции чисел)
/// d obj.Do("*",.result)
/// ; конкатенация с разделителем (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("_",.result,separator)
/// ; минимум (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("min",.result)
/// ; максимум (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("max",.result)
/// ; среднее (имеет смысл лишь для коллекции чисел)
/// d obj.Do("avg",.result)
/// ; любой код, где el=элемент коллекции, args=переданные аргументы
/// d obj.Do($lb("s args(1,1)=args(1,1)+el"),.result) ; эквивалент "+"
/// ; вызов подпрограммы sub^prog с передачей аргументов
/// d obj.Do($lb("d sub^prog(el,args...)"),.result,param1,param2,paramN)
/// </example>
/// 
Method 
Do(func "+"Args...As %Status
{
  
#define ReturnOnError(%expr) s sc=%expr ret:$$$ISERR(scsc
  
    quit
:'..collection.Count() $$$OK
    
    if 
func="+" {
        
set func=$listbuild("s args(1,1)=args(1,1)+el")
    
}elseif func="*" {
        
set func=$listbuild("s args(1,1)=args(1,1)*el")
    
}elseif func="_" {
        
set func=$listbuild("s args(1,1)=args(1,1)_args(1,2)_el")
    
}elseif func="min" {
        
set func=$listbuild("s:el<args(1,1) args(1,1)=el"),Args(1)=999999999999999
    
}elseif func="max" {
        
set func=$listbuild("s:el>args(1,1) args(1,1)=el"),Args(1)=-999999999999999
    
}elseif func="avg" {
        
set func=$listbuild("s args(1,1)=el/args(1,2)+args(1,1)"),Args=2,Args(2)=..collection.Count() kill Args(1)
    
}
    
    
if $listvalid(func{
        
set cmd=$list(func)
        
        
$$$ReturnOnError(##class(%Routine).CheckSyntax(" "_cmd))
        
        
set cmd="(el,args...){"_cmd_"}"
        
      
set key ""
      
for {
          
set el = ..collection.GetNext(.key)
          
quit:key=""
        
xecute (cmd,el,.Args)
      
}
    }
else{
        
if func[":" {
          
set className=$piece(func,":",1)
          
set methodName=$piece(func,":",2)
          
quit:'##class(%Dictionary.MethodDefinition).IDKEYExists(className,methodName$$$ERROR($$$MethodDoesNotExist,func)
          
quit:'$$$defMemberKeyGet(className,"m",methodName,23) $$$ERROR($$$GeneralError,$$$FormatText("Метод %1 не является методом класса %2",methodName,className))
          
set key ""
          
for {
              
set el = ..collection.GetNext(.key)
              
quit:key=""
            
$$$ReturnOnError($classmethod(className,methodName,el,.Args))
          
}
        }
else{
          
set methodName=func
          
set key ""
          
for {
              
set el = ..collection.GetNext(.key)
              
quit:key=""
              
set className=$classname(el)
              
return:'##class(%Dictionary.MethodDefinition).IDKEYExists(className,methodName$$$ERROR($$$MethodDoesNotExist,className_":"_methodName)
              
return:$$$defMemberKeyGet(className,"m",methodName,23) $$$ERROR($$$GeneralError,$$$FormatText("Метод %1 не является методом экземпляра класса %2",methodName,className))
            
$$$ReturnOnError($method(el,methodName,.Args))
          
}
        }
    }
    
    
quit $$$OK
}
/// <example>d ##class(test.ForEach).Test()</example>
ClassMethod 
Test() [ Internal ]
{
    
set old=$system.Process.Undefined(2)
    
try{
        
;==============================  КОЛЛЛЕКЦИЯ ПРОСТЫХ ТИПОВ ДАННЫХ  =============================
        
set t=##class(test.ForEach).%New("2,3,5")
        
;s t=##class(test.ForEach).%New("2,3,5,asd")
        ;s t=##class(test.ForEach).%New(##class(test.ForEach).%New()) ; раскомментируйте, чтобы увидеть ошибку
        
if '$IsObject(t$$$ThrowStatus(%objlasterror)
        
        
write !,"==========",!,"test.myclass:Dump",!!!
        
$$$ThrowOnError(t.Do("test.myclass:Dump")) 
        
; или $$$ThrowOnError(t.Do("test.myclass:Dump",.result))
        
        
write !,"==========",!,"test.myclass:Dump(.r=""result"",""p1"",""p2"")",!!!
        
set r="result" $$$ThrowOnError(t.Do("test.myclass:Dump",.r,"p1","p2"))
        
        
write !,"==========",!,"test.myclass:Sum(.r)",!!!
        
$$$ThrowOnError(t.Do("test.myclass:Sum",.r)) write "Результат = ",r,!
        
;$$$ThrowOnError(t.Do("test.myclass:Sum",.r,5)) ; раскомментируйте, чтобы увидеть ошибку
        
        
write !,"==========",!,"+10",!! set r=10
        
$$$ThrowOnError(t.Do(,.r)) write "Результат = ",r,!
        
write !,"==========",!,"+",!! kill r
        
$$$ThrowOnError(t.Do(,.r)) write "Результат = ",r,!
        
write !,"==========",!,"*",!! set r=1
        
$$$ThrowOnError(t.Do("*",.r)) write "Результат = ",r,!
        
write !,"==========",!,"_ + разделитель=""^""",!! kill r
        
$$$ThrowOnError(t.Do("_",.r,"^")) write "Результат = ",r,!
        
        
write !,"==========",!,"min (входной аргумент не учитывается)",!!!
        
set r="asd" $$$ThrowOnError(t.Do("min",.r)) write "Результат = ",r,!
        
write !,"==========",!,"max (входной аргумент не учитывается)",!!!
        
set r="asd" $$$ThrowOnError(t.Do("max",.r)) write "Результат = ",r,!
        
write !,"==========",!,"avg (входной аргумент не учитывается)",!!!
        
set r="asd" $$$ThrowOnError(t.Do("avg",.r)) write "Результат = ",r,!
        
        
write !,"==========",!,"s args(1,1)=args(1,1)+el",!!!
        
kill $$$ThrowOnError(t.Do($listbuild("s args(1,1)=args(1,1)+el"),.r))    write r,!
        
        
write !,"==========",!,"d sub^prog(el,args...) [.r=""r"",""p1"",""p2""]",!!!
        
set r="r"    $$$ThrowOnError(t.Do($listbuild("d sub^prog(el,args...)"),.r,"p1","p2"))
        
;==============================  КОЛЛЛЕКЦИЯ СЛОЖНЫХ ТИПОВ ДАННЫХ  =============================
        
set list=##class(%ListOfObjects).%New()
        
for i="f1","f2","f3" do list.Insert(##class(test.myclass).%New(i))
        
;f i="f1","f2","f3",7 d list.Insert(##class(test.myclass).%New(i))
        
        
set t=##class(test.ForEach).%New(list)
        
if '$IsObject(t$$$ThrowStatus(%objlasterror)
        
        
write !,"++++++++++",!,"test.myclass:Dump",!!!
        
$$$ThrowOnError(t.Do("test.myclass:Dump"))
        
        
write !,"++++++++++",!,"PrintLn",!!!
        
$$$ThrowOnError(t.Do("PrintLn"))
        
        
write !,"++++++++++",!,"PrintLn(,""Элемент = "")",!!!
        
$$$ThrowOnError(t.Do("PrintLn",,"Элемент = "))
        
        
write !,"++++++++++",!,"Concat(.r)",!! kill r
        
$$$ThrowOnError(t.Do("Concat",.r)) write "Результат = ",r,!
        
;$$$ThrowOnError(t.Do("Concat",.r,"f3")) w "Результат = ",r,! ; раскомментируйте, чтобы увидеть ошибку
        
write !,"++++++++++",!,"SetField(,""blablabla"") + PrintLn(,""Элемент = "")",!!!
        
$$$ThrowOnError(t.Do("SetField",,"blablabla")) $$$ThrowOnError(t.Do("PrintLn",,"Элемент = "))
        
        
write !,"++++++++++",!,"d el.PrintLn(.args)",!!!
        
$$$ThrowOnError(t.Do($listbuild("d el.PrintLn(.args)")))
        
        
write !,"++++++++++",!,"w ""field="",el.field,!",!!!
        
$$$ThrowOnError(t.Do($listbuild("w ""field="",el.field,!")))
    
}catch(ex){
        
#dim ex As %Exception.AbstractException
        
write ex.DisplayString()
    
}
    
do $system.Process.Undefined(old)
}
}


В коде класса применялись некоторые возможности COS:
  • Args... (передача произвольного числа аргументов в метод/процедуру/программу);
  • XECUTE или $XECUTE (выполнение произвольных команд COS);
  • $COMPILE (компиляция/проверка синтаксиса кода);
  • $CLASSMETHOD (вызов произвольного метода класса с передачей произвольного числа аргументов);
  • $METHOD (вызов произвольного метода экземпляра класса с передачей произвольного числа аргументов);
  • Библиотека встроенных классов.


Внимание! Все приведенные ниже примеры предполагают, что Undefined=2.

Этот режим можно установить в терминале.
> set old=$system.Process.Undefined(2)

Выполнить тесты и не забыть потом вернуть на место
> do $system.Process.Undefined(old)


Исходный код класса test.myclass
Class test.myclass Extends %RegisteredObject
{
/// Строковое поле.
Property 
field;
/// Инициализация свойства <property>field</property>.
Method 
%OnNew(fieldAs %Status InternalPrivateServerOnly = 1 ]
{
    
set ..field=field
    
quit $$$OK
}
/// Заполнение <property>field</property> первым <u>входным</u> аргументом.
Method 
SetField(Args...As %Status
{
    
set ..field=Args(1,2)
    
quit $$$OK
}
/// Вывод <property>field</property> и первого <u>входного</u> аргумента.
Method 
PrintLn(Args...As %Status
{
    
write Args(1,2),$$$quote(..field),!
    
quit $$$OK
}
/// Конкатенация <property>field</property> с разделителем (<span style="color:green;">метод <b>экземпляра</b> класса</span>).
/// <br>Если первый входной аргумент совпадает с <var>field</var>, генерируем ошибку (<span style="color:red;">для демонстрационных целей!</span>)
Method 
Concat(Args...As %Status
{
    
set Args(1,1)=Args(1,1)_Args(1,2)_..field
    quit $select
(..field=Args(1,2):$$$ERROR($$$GeneralError,$$$FormatText("Возникла ошибка на элементе: %1",..field)),1:$$$OK)
}
/// Сумма <var>elem</var> (<span style="color:green;">метод класса</span>).
/// <br>Если первый <u>входной</u> аргумент совпадает с <var>elem</var> (он же <property>field</property>), генерируем ошибку (<span style="color:red;">для демонстрационных целей!</span>)
ClassMethod 
Sum(elemArgs...As %Status
{
    
set Args(1,1)=Args(1,1)+elem
    
quit $select(elem=Args(1,2):$$$ERROR($$$GeneralError,$$$FormatText("Возникла ошибка на элементе: %1",elem)),1:$$$OK)
}
/// Вывод всех аргументов.
/// <br><br> <var>elem</var> = элемент коллекции
/// <br> <var>Args</var>(1) = кол-во переданных аргументов кроме первого, т.е. <var>elem</var>
/// <br> <var>Args</var>(1,1) = аргумент 1 (<span style="color:red;">у нас это входной/выходной аргумент</span>)
/// <br> <var>Args</var>(1,2) = аргумент 2
/// <br> …
/// <br> <var>Args</var>(1,n) = аргумент n
ClassMethod 
Dump(elemArgs...As %Status
{
    
set params=""
    
    
for i=2:1:Args(1)    set params=params_$listbuild(Args(1,i))
    
if '$IsObject(elem{
        
set el=elem
    
}elseif elem.%Extends("test.myclass"){
        
set el=elem.field
    
}else{
        
set el=elem.%ClassName($$$YES)
    
}
    
write "Элемент = ",$$$quote(el),", Выходной аргумент = ",$$$quote(Args(1,1)),", Дополнительные аргументы = ",$$$quote($listtostring(params)),!
    
quit $$$OK
}
}


Исходный код программы prog.mac
    #include %systemInclude
    
sub(el,args...) public {
    
write "--------",!,"el = ",$$$quote(el),!
    
zwrite args
    
write !
}


Поехали!
Инициализация объекта ForEach
Инициализация происходит с помощью метода test.ForEach.%New(val,sep)
Первый параметр val принимает коллекцию литералов, либо список, либо коллекцию объектов.
Второй параметр sep — разделитель коллекции литералов.

1. Инициализация коллекции литералов

set tmp=##class(test.ForEach).%New("2,3,5")
или
set tmp=##class(test.ForEach).%New($listbuild(2,3,5))


2. Инициализация коллекции литералов через произвольный разделитель
Например через разделитель ";"

set tmp=##class(test.ForEach).%New("2;zxc;5;asd,ert",";")
или
set tmp=##class(test.ForEach).%New($listbuild(2,"zxc",5,"asd,ert"))


3. Инициализация списка объектов
set list=##class(%ListOfObjects).%New()
for i="f1","f2","f3",7 do list.Insert(##class(test.myclass).%New(i))
set tmp=##class(test.ForEach).%New(list)


Внимание! Класс test.ForEach в методе %New ожидает коллекцию-наследника от %Collection.AbstractList

Примеры использования


В классе test.myclass реализованы несколько методов, которые мы будем вызывать для каждого из элементов коллекции.
Например Dump — выводит информацию об элементе и переданных параметрах.
Sum — суммирует аргументы, выводит результат.

Примеры с коллекцией чисел
Инициализируем коллекцию:
set tmp=##class(test.ForEach).%New("2,3,5")


Выполним в терминале:
>do tmp.Do("test.myclass:Dump")
Элемент = 2, Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = 3, Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = 5, Выходной аргумент = "", Дополнительные аргументы = ""

>set r="result" do tmp.Do("test.myclass:Dump",.r,"p1","p2")
Элемент = 2, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"
Элемент = 3, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"
Элемент = 5, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"



Остальные примеры с числами

>kill r do tmp.Do("test.myclass:Sum",.r) write r
10

>kill r do $system.OBJ.DisplayError(tmp.Do("test.myclass:Sum",.r,5))
ОШИБКА #5001: Возникла ошибка на элементе: 5

>do $system.OBJ.DisplayError(tmp.Do("PrintLn"))
ОШИБКА #5654: Метод '2:PrintLn' не существует

>do $system.OBJ.DisplayError(tmp.Do("test.myclass:PrintLn"))
ОШИБКА #5001: Метод PrintLn не является методом класса test.myclass

>set r=10 do tmp.Do(,.r) write r
20 (=10 +2+3+5)

>kill r do tmp.Do(,.r) write r
10 (=2+3+5)

>set r=-10 do tmp.Do("+",.r) write r
0 (=-10 +2+3+5)

>set r=1 do tmp.Do("*",.r) write r
30 (=2*3*5)

>kill r do tmp.Do("_",.r,"^") write r
^2^3^5 (склейка с разделителем)

>do tmp.Do("min",.r) write r
2 (минимум)

>do tmp.Do("max",.r) write r
5 (максимум)

>do tmp.Do("avg",.r) write r
3.333333333333333334 (=(2+3+5)/3)
>kill r do tmp.Do($listbuild("set args(1,1)=args(1,1)+el"),.r) write r
10 (=2+3+5)

>set r="r" do tmp.Do($listbuild("do sub^prog(el,args...)"),.r,"p1","p2")
--------
el = 2
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"
 
--------
el = 3
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"
 
--------
el = 5
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"

>set r="r" do tmp.Do($listbuild("do1 sub^prog(el,args...)"),.r,"p1","p2")
ОШИБКА #5745: Ошибка компиляции!



Примеры использования для коллекции объектов

Инициализация:
set list=##class(%ListOfObjects).%New()
for i="f1","f2","f3" do list.Insert(##class(test.myclass).%New(i))
set tmp=##class(test.ForEach).%New(list)


Проверка в терминале:
>do tmp.Do("test.myclass:Dump")
Элемент = "f1", Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = "f2", Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = "f3", Выходной аргумент = "", Дополнительные аргументы = ""

>do tmp.Do("PrintLn")
"f1"
"f2"
"f3"

>do tmp.Do("PrintLn",,"Элемент = ")
Элемент = "f1"
Элемент = "f2"
Элемент = "f3"

>kill r do tmp.Do("Concat",.r,"**") write r
**f1**f2**f3

>kill r do $system.OBJ.DisplayError(tmp.Do("Concat",.r,"f3"))
ОШИБКА #5001: Возникла ошибка на элементе: f3

>do $system.OBJ.DisplayError(tmp.Do("PrintLn1"))
ОШИБКА #5654: Метод 'test.myclass:PrintLn1' не существует

>do $system.OBJ.DisplayError(tmp.Do("Sum",.r))
ОШИБКА #5001: Метод Sum не является методом экземпляра класса test.myclass

>do tmp.Do("SetField",,"blablabla"), tmp.Do("PrintLn",,"Элемент = ")
Элемент = "blablabla"
Элемент = "blablabla"
Элемент = "blablabla"

>do tmp.Do($listbuild("do el.PrintLn(.args)"))
"blablabla"
"blablabla"
"blablabla"

>do tmp.Do($listbuild("write ""field="",el.field,!"))
field=blablabla
field=blablabla
field=blablabla


Без внимания остались другие типы коллекций, например: массивы, глобалы, таблицы, потоки. Но зато теперь вы знаете «как это работает»…

Исходники классов и примеров.

Disclaimer: данная статья опубликована с разрешения автора, пожелавшего остаться неизвестным.

Спасибо за внимание!
Tags:
Hubs:
+8
Comments 6
Comments Comments 6

Articles

Information

Website
www.intersystems.com
Registered
Founded
1978
Employees
1,001–5,000 employees
Location
США